diff --git a/backend-node/src/controllers/entityJoinController.ts b/backend-node/src/controllers/entityJoinController.ts index fbb88750..013b2034 100644 --- a/backend-node/src/controllers/entityJoinController.ts +++ b/backend-node/src/controllers/entityJoinController.ts @@ -30,6 +30,7 @@ export class EntityJoinController { autoFilter, // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ž๋™ ํ•„ํ„ฐ dataFilter, // ๐Ÿ†• ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ (JSON ๋ฌธ์ž์—ด) excludeFilter, // ๐Ÿ†• ์ œ์™ธ ํ•„ํ„ฐ (JSON ๋ฌธ์ž์—ด) - ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋ฐ์ดํ„ฐ ์ œ์™ธ + deduplication, // ๐Ÿ†• ์ค‘๋ณต ์ œ๊ฑฐ ์„ค์ • (JSON ๋ฌธ์ž์—ด) userLang, // userLang์€ ๋ณ„๋„๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ search์— ํฌํ•จ๋˜์ง€ ์•Š๋„๋ก ํ•จ ...otherParams } = req.query; @@ -139,6 +140,24 @@ export class EntityJoinController { } } + // ๐Ÿ†• ์ค‘๋ณต ์ œ๊ฑฐ ์„ค์ • ์ฒ˜๋ฆฌ + let parsedDeduplication: { + enabled: boolean; + groupByColumn: string; + keepStrategy: "latest" | "earliest" | "base_price" | "current_date"; + sortColumn?: string; + } | undefined = undefined; + if (deduplication) { + try { + parsedDeduplication = + typeof deduplication === "string" ? JSON.parse(deduplication) : deduplication; + logger.info("์ค‘๋ณต ์ œ๊ฑฐ ์„ค์ • ํŒŒ์‹ฑ ์™„๋ฃŒ:", parsedDeduplication); + } catch (error) { + logger.warn("์ค‘๋ณต ์ œ๊ฑฐ ์„ค์ • ํŒŒ์‹ฑ ์˜ค๋ฅ˜:", error); + parsedDeduplication = undefined; + } + } + const result = await tableManagementService.getTableDataWithEntityJoins( tableName, { @@ -156,13 +175,26 @@ export class EntityJoinController { screenEntityConfigs: parsedScreenEntityConfigs, dataFilter: parsedDataFilter, // ๐Ÿ†• ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ ์ „๋‹ฌ excludeFilter: parsedExcludeFilter, // ๐Ÿ†• ์ œ์™ธ ํ•„ํ„ฐ ์ „๋‹ฌ + deduplication: parsedDeduplication, // ๐Ÿ†• ์ค‘๋ณต ์ œ๊ฑฐ ์„ค์ • ์ „๋‹ฌ } ); + // ๐Ÿ†• ์ค‘๋ณต ์ œ๊ฑฐ ์ฒ˜๋ฆฌ (๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ์— ์ ์šฉ) + let finalData = result; + if (parsedDeduplication?.enabled && parsedDeduplication.groupByColumn && Array.isArray(result.data)) { + logger.info(`๐Ÿ”„ ์ค‘๋ณต ์ œ๊ฑฐ ์‹œ์ž‘: ๊ธฐ์ค€ ์ปฌ๋Ÿผ = ${parsedDeduplication.groupByColumn}, ์ „๋žต = ${parsedDeduplication.keepStrategy}`); + const originalCount = result.data.length; + finalData = { + ...result, + data: this.deduplicateData(result.data, parsedDeduplication), + }; + logger.info(`โœ… ์ค‘๋ณต ์ œ๊ฑฐ ์™„๋ฃŒ: ${originalCount}๊ฐœ โ†’ ${finalData.data.length}๊ฐœ`); + } + res.status(200).json({ success: true, message: "Entity ์กฐ์ธ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์„ฑ๊ณต", - data: result, + data: finalData, }); } catch (error) { logger.error("Entity ์กฐ์ธ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹คํŒจ", error); @@ -537,6 +569,98 @@ export class EntityJoinController { }); } } + + /** + * ์ค‘๋ณต ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ (๋ฉ”๋ชจ๋ฆฌ ๋‚ด ์ฒ˜๋ฆฌ) + */ + private deduplicateData( + data: any[], + config: { + groupByColumn: string; + keepStrategy: "latest" | "earliest" | "base_price" | "current_date"; + sortColumn?: string; + } + ): any[] { + if (!data || data.length === 0) return data; + + // ๊ทธ๋ฃน๋ณ„๋กœ ๋ฐ์ดํ„ฐ ๋ถ„๋ฅ˜ + const groups: Record = {}; + + for (const row of data) { + const groupKey = row[config.groupByColumn]; + if (groupKey === undefined || groupKey === null) continue; + + if (!groups[groupKey]) { + groups[groupKey] = []; + } + groups[groupKey].push(row); + } + + // ๊ฐ ๊ทธ๋ฃน์—์„œ ํ•˜๋‚˜์˜ ํ–‰๋งŒ ์„ ํƒ + const result: any[] = []; + + for (const [groupKey, rows] of Object.entries(groups)) { + if (rows.length === 0) continue; + + let selectedRow: any; + + switch (config.keepStrategy) { + case "latest": + // ์ •๋ ฌ ์ปฌ๋Ÿผ ๊ธฐ์ค€ ์ตœ์‹  (๊ฐ€์žฅ ํฐ ๊ฐ’) + if (config.sortColumn) { + rows.sort((a, b) => { + const aVal = a[config.sortColumn!]; + const bVal = b[config.sortColumn!]; + if (aVal === bVal) return 0; + if (aVal > bVal) return -1; + return 1; + }); + } + selectedRow = rows[0]; + break; + + case "earliest": + // ์ •๋ ฌ ์ปฌ๋Ÿผ ๊ธฐ์ค€ ์ตœ์ดˆ (๊ฐ€์žฅ ์ž‘์€ ๊ฐ’) + if (config.sortColumn) { + rows.sort((a, b) => { + const aVal = a[config.sortColumn!]; + const bVal = b[config.sortColumn!]; + if (aVal === bVal) return 0; + if (aVal < bVal) return -1; + return 1; + }); + } + selectedRow = rows[0]; + break; + + case "base_price": + // base_price๊ฐ€ true์ธ ํ–‰ ์„ ํƒ + selectedRow = rows.find((r) => r.base_price === true || r.base_price === "true") || rows[0]; + break; + + case "current_date": + // ์˜ค๋Š˜ ๋‚ ์งœ ๊ธฐ์ค€ ์œ ํšจ ๊ธฐ๊ฐ„ ๋‚ด ํ–‰ ์„ ํƒ + const today = new Date().toISOString().split("T")[0]; + selectedRow = rows.find((r) => { + const startDate = r.start_date; + const endDate = r.end_date; + if (!startDate) return true; + if (startDate <= today && (!endDate || endDate >= today)) return true; + return false; + }) || rows[0]; + break; + + default: + selectedRow = rows[0]; + } + + if (selectedRow) { + result.push(selectedRow); + } + } + + return result; + } } export const entityJoinController = new EntityJoinController(); diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index b21a7c61..70ed6205 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -3800,6 +3800,15 @@ export class TableManagementService { const cacheableJoins: EntityJoinConfig[] = []; const dbJoins: EntityJoinConfig[] = []; + // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ํšŒ์‚ฌ๋ณ„ ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ”์€ ์บ์‹œ ์‚ฌ์šฉ ๋ถˆ๊ฐ€ (company_code ํ•„ํ„ฐ๋ง ํ•„์š”) + const companySpecificTables = [ + "supplier_mng", + "customer_mng", + "item_info", + "dept_info", + // ํ•„์š”์‹œ ์ถ”๊ฐ€ + ]; + for (const config of joinConfigs) { // table_column_category_values๋Š” ํŠน์ˆ˜ ์กฐ์ธ ์กฐ๊ฑด์ด ํ•„์š”ํ•˜๋ฏ€๋กœ ํ•ญ์ƒ DB ์กฐ์ธ if (config.referenceTable === "table_column_category_values") { @@ -3808,6 +3817,13 @@ export class TableManagementService { continue; } + // ๐Ÿ”’ ํšŒ์‚ฌ๋ณ„ ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ”์€ ์บ์‹œ ์‚ฌ์šฉ ๋ถˆ๊ฐ€ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ) + if (companySpecificTables.includes(config.referenceTable)) { + dbJoins.push(config); + console.log(`๐Ÿ”— DB ์กฐ์ธ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ): ${config.referenceTable}`); + continue; + } + // ์บ์‹œ ๊ฐ€๋Šฅ์„ฑ ํ™•์ธ const cachedData = await referenceCacheService.getCachedReference( config.referenceTable, diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index f41c62af..f7926f43 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -174,8 +174,16 @@ export const ScreenModal: React.FC = ({ className }) => { // ๐Ÿ†• editData๊ฐ€ ์žˆ์œผ๋ฉด formData์™€ originalData๋กœ ์„ค์ • (์ˆ˜์ • ๋ชจ๋“œ) if (editData) { console.log("๐Ÿ“ [ScreenModal] ์ˆ˜์ • ๋ฐ์ดํ„ฐ ์„ค์ •:", editData); - setFormData(editData); - setOriginalData(editData); // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ์ €์žฅ (UPDATE ํŒ๋‹จ์šฉ) + + // ๐Ÿ†• ๋ฐฐ์—ด์ธ ๊ฒฝ์šฐ (๊ทธ๋ฃน ๋ ˆ์ฝ”๋“œ) vs ๋‹จ์ผ ๊ฐ์ฒด ์ฒ˜๋ฆฌ + if (Array.isArray(editData)) { + console.log(`๐Ÿ“ [ScreenModal] ๊ทธ๋ฃน ๋ ˆ์ฝ”๋“œ ${editData.length}๊ฐœ ์„ค์ •`); + setFormData(editData as any); // ๋ฐฐ์—ด ๊ทธ๋Œ€๋กœ ์ „๋‹ฌ (SelectedItemsDetailInput์—์„œ ์ฒ˜๋ฆฌ) + setOriginalData(editData[0] || null); // ์ฒซ ๋ฒˆ์งธ ๋ ˆ์ฝ”๋“œ๋ฅผ ์›๋ณธ์œผ๋กœ ์ €์žฅ + } else { + setFormData(editData); + setOriginalData(editData); // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ์ €์žฅ (UPDATE ํŒ๋‹จ์šฉ) + } } else { // ๐Ÿ†• ์‹ ๊ทœ ๋“ฑ๋ก ๋ชจ๋“œ: ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๋ฏธ๋ฆฌ ์„ค์ • // ๐Ÿ”ง ์ค‘์š”: ์‹ ๊ทœ ๋“ฑ๋ก ์‹œ์—๋Š” ์—ฐ๊ฒฐ ํ•„๋“œ(equipment_code ๋“ฑ)๋งŒ ์ „๋‹ฌํ•ด์•ผ ํ•จ diff --git a/frontend/lib/api/entityJoin.ts b/frontend/lib/api/entityJoin.ts index a3206df9..1e84588d 100644 --- a/frontend/lib/api/entityJoin.ts +++ b/frontend/lib/api/entityJoin.ts @@ -77,6 +77,12 @@ export const entityJoinApi = { filterColumn?: string; filterValue?: any; }; // ๐Ÿ†• ์ œ์™ธ ํ•„ํ„ฐ (๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋ฐ์ดํ„ฐ ์ œ์™ธ) + deduplication?: { + enabled: boolean; + groupByColumn: string; + keepStrategy: "latest" | "earliest" | "base_price" | "current_date"; + sortColumn?: string; + }; // ๐Ÿ†• ์ค‘๋ณต ์ œ๊ฑฐ ์„ค์ • } = {}, ): Promise => { // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: company_code ์ž๋™ ํ•„ํ„ฐ๋ง ํ™œ์„ฑํ™” @@ -99,6 +105,7 @@ export const entityJoinApi = { autoFilter: JSON.stringify(autoFilter), // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„ํ„ฐ๋ง dataFilter: params.dataFilter ? JSON.stringify(params.dataFilter) : undefined, // ๐Ÿ†• ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ excludeFilter: params.excludeFilter ? JSON.stringify(params.excludeFilter) : undefined, // ๐Ÿ†• ์ œ์™ธ ํ•„ํ„ฐ + deduplication: params.deduplication ? JSON.stringify(params.deduplication) : undefined, // ๐Ÿ†• ์ค‘๋ณต ์ œ๊ฑฐ ์„ค์ • }, }); return response.data.data; diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 8a4ebe8f..9da76559 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"; @@ -169,6 +170,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>({}); // ์ขŒ์ธก ์ปฌ๋Ÿผ ๋ผ๋ฒจ @@ -610,6 +616,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( ( @@ -960,11 +1001,12 @@ export const SplitPanelLayoutComponent: React.FC console.log("๐Ÿ”— [๋ถ„ํ• ํŒจ๋„] ๋ณตํ•ฉํ‚ค ์กฐ๊ฑด:", searchConditions); - // ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ API๋กœ ๋ฐ์ดํ„ฐ ์กฐํšŒ + // ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ API๋กœ ๋ฐ์ดํ„ฐ ์กฐํšŒ (๐Ÿ†• deduplication ์ „๋‹ฌ) const result = await entityJoinApi.getTableDataWithJoins(rightTableName, { search: searchConditions, enableEntityJoin: true, size: 1000, + deduplication: componentConfig.rightPanel?.deduplication, // ๐Ÿ†• ์ค‘๋ณต ์ œ๊ฑฐ ์„ค์ • ์ „๋‹ฌ }); console.log("๐Ÿ”— [๋ถ„ํ• ํŒจ๋„] ๋ณตํ•ฉํ‚ค ์กฐํšŒ ๊ฒฐ๊ณผ:", result); @@ -1037,12 +1079,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; @@ -1053,7 +1220,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], ); // ์šฐ์ธก ํ•ญ๋ชฉ ํ™•์žฅ/์ถ•์†Œ ํ† ๊ธ€ @@ -1449,14 +1639,19 @@ export const SplitPanelLayoutComponent: React.FC // ์ˆ˜์ • ๋ฒ„ํŠผ ํ•ธ๋“ค๋Ÿฌ const handleEditClick = useCallback( - (panel: "left" | "right", item: any) => { + async (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 || ""; console.log("โœ… ์ˆ˜์ • ๋ชจ๋‹ฌ ์—ด๊ธฐ:", { tableName: rightTableName, @@ -1470,33 +1665,108 @@ 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, }); + // ๐Ÿ†• groupByColumns ๊ธฐ์ค€์œผ๋กœ ๋ชจ๋“  ๊ด€๋ จ ๋ ˆ์ฝ”๋“œ ์กฐํšŒ (API ์ง์ ‘ ํ˜ธ์ถœ) + let allRelatedRecords = [item]; // ๊ธฐ๋ณธ๊ฐ’: ํ˜„์žฌ ์•„์ดํ…œ๋งŒ + + if (groupByColumns.length > 0) { + // groupByColumns ๊ฐ’์œผ๋กœ ๊ฒ€์ƒ‰ ์กฐ๊ฑด ์ƒ์„ฑ + const matchConditions: Record = {}; + groupByColumns.forEach((col: string) => { + if (item[col] !== undefined && item[col] !== null) { + matchConditions[col] = item[col]; + } + }); + + console.log("๐Ÿ” [SplitPanel] ๊ทธ๋ฃน ๋ ˆ์ฝ”๋“œ ์กฐํšŒ ์‹œ์ž‘:", { + ํ…Œ์ด๋ธ”: rightTableName, + ์กฐ๊ฑด: matchConditions, + }); + + if (Object.keys(matchConditions).length > 0) { + // ๐Ÿ†• deduplication ์—†์ด ์›๋ณธ ๋ฐ์ดํ„ฐ ๋‹ค์‹œ ์กฐํšŒ (API ์ง์ ‘ ํ˜ธ์ถœ) + try { + const { entityJoinApi } = await import("@/lib/api/entityJoin"); + + // ๐Ÿ”ง dataFilter๋กœ ์ •ํ™• ๋งค์นญ ์กฐ๊ฑด ์ƒ์„ฑ (search๋Š” LIKE ๊ฒ€์ƒ‰์ด๋ผ ๋ถ€์ •ํ™•) + const exactMatchFilters = Object.entries(matchConditions).map(([key, value]) => ({ + id: `exact-${key}`, + columnName: key, + operator: "equals", + value: value, + valueType: "text", + })); + + console.log("๐Ÿ” [SplitPanel] ์ •ํ™• ๋งค์นญ ํ•„ํ„ฐ:", exactMatchFilters); + + const result = await entityJoinApi.getTableDataWithJoins(rightTableName, { + // search ๋Œ€์‹  dataFilter ์‚ฌ์šฉ (์ •ํ™• ๋งค์นญ) + dataFilter: { + enabled: true, + matchType: "all", + filters: exactMatchFilters, + }, + enableEntityJoin: true, + size: 1000, + // ๐Ÿ”ง ๋ช…์‹œ์ ์œผ๋กœ deduplication ๋น„ํ™œ์„ฑํ™” (๋ชจ๋“  ๋ ˆ์ฝ”๋“œ ๊ฐ€์ ธ์˜ค๊ธฐ) + deduplication: { enabled: false, groupByColumn: "", keepStrategy: "latest" }, + }); + + // ๐Ÿ” ๋””๋ฒ„๊น…: API ์‘๋‹ต ๊ตฌ์กฐ ํ™•์ธ + console.log("๐Ÿ” [SplitPanel] API ์‘๋‹ต ์ „์ฒด:", result); + console.log("๐Ÿ” [SplitPanel] result.data:", result.data); + console.log("๐Ÿ” [SplitPanel] result ํƒ€์ž…:", typeof result); + + // result ์ž์ฒด๊ฐ€ ๋ฐฐ์—ด์ผ ์ˆ˜๋„ ์žˆ์Œ (entityJoinApi ์‘๋‹ต ๊ตฌ์กฐ์— ๋”ฐ๋ผ) + const dataArray = Array.isArray(result) ? result : (result.data || []); + + if (dataArray.length > 0) { + allRelatedRecords = dataArray; + console.log("โœ… [SplitPanel] ๊ทธ๋ฃน ๋ ˆ์ฝ”๋“œ ์กฐํšŒ ์™„๋ฃŒ:", { + ์กฐ๊ฑด: matchConditions, + ๊ฒฐ๊ณผ์ˆ˜: allRelatedRecords.length, + ๋ ˆ์ฝ”๋“œ๋“ค: allRelatedRecords.map((r: any) => ({ id: r.id, supplier_item_code: r.supplier_item_code })), + }); + } else { + console.warn("โš ๏ธ [SplitPanel] ๊ทธ๋ฃน ๋ ˆ์ฝ”๋“œ ์กฐํšŒ ๊ฒฐ๊ณผ ์—†์Œ, ํ˜„์žฌ ์•„์ดํ…œ๋งŒ ์‚ฌ์šฉ"); + } + } catch (error) { + console.error("โŒ [SplitPanel] ๊ทธ๋ฃน ๋ ˆ์ฝ”๋“œ ์กฐํšŒ ์‹คํŒจ:", error); + allRelatedRecords = [item]; + } + } else { + console.warn("โš ๏ธ [SplitPanel] groupByColumns ๊ฐ’์ด ์—†์Œ, ํ˜„์žฌ ์•„์ดํ…œ๋งŒ ์‚ฌ์šฉ"); + } + } + // ๐Ÿ”ง ์ˆ˜์ •: URL ํŒŒ๋ผ๋ฏธํ„ฐ ๋Œ€์‹  editData๋กœ ์ง์ ‘ ์ „๋‹ฌ // ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ…Œ์ด๋ธ”์˜ Primary Key๊ฐ€ ๋ฌด์—‡์ด๋“  ์ƒ๊ด€์—†์ด ๋ฐ์ดํ„ฐ๊ฐ€ ์ •ํ™•ํžˆ ์ „๋‹ฌ๋จ window.dispatchEvent( new CustomEvent("openScreenModal", { detail: { screenId: modalScreenId, - editData: item, // ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ์ง์ ‘ ์ „๋‹ฌ - ...(groupByColumns.length > 0 && { - urlParams: { + editData: allRelatedRecords, // ๐Ÿ†• ๋ชจ๋“  ๊ด€๋ จ ๋ ˆ์ฝ”๋“œ ์ „๋‹ฌ (๋ฐฐ์—ด) + urlParams: { + mode: "edit", // ๐Ÿ†• ์ˆ˜์ • ๋ชจ๋“œ ํ‘œ์‹œ + ...(groupByColumns.length > 0 && { groupByColumns: JSON.stringify(groupByColumns), - }, - }), + }), + }, }, }), ); console.log("โœ… [SplitPanel] openScreenModal ์ด๋ฒคํŠธ ๋ฐœ์ƒ (editData ์ง์ ‘ ์ „๋‹ฌ):", { screenId: modalScreenId, - editData: item, + editData: allRelatedRecords, + recordCount: allRelatedRecords.length, groupByColumns: groupByColumns.length > 0 ? JSON.stringify(groupByColumns) : "์—†์Œ", }); @@ -1510,7 +1780,7 @@ export const SplitPanelLayoutComponent: React.FC setEditModalFormData({ ...item }); setShowEditModal(true); }, - [componentConfig], + [componentConfig, activeTabIndex], ); // ์ˆ˜์ • ๋ชจ๋‹ฌ ์ €์žฅ @@ -1610,13 +1880,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); } @@ -1746,7 +2022,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({ @@ -1770,7 +2051,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( @@ -2591,6 +2872,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 && ( + + )} {/* ์šฐ์ธก ํŒจ๋„ ์ˆ˜์ •/์‚ญ์ œ๋Š” ๊ฐ ์นด๋“œ์—์„œ ์ฒ˜๋ฆฌ */}
)} @@ -2632,16 +2953,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) ? ( // ์กฐ์ธ ๋ชจ๋“œ: ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ๋ฅผ ํ…Œ์ด๋ธ”/๋ฆฌ์ŠคํŠธ๋กœ ํ‘œ์‹œ @@ -3084,6 +3617,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 97f7b56a..9810388f 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 ์„ค์ • ํŒจ๋„ */ @@ -2854,6 +3697,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[]; }; // ๋ ˆ์ด์•„์›ƒ ์„ค์ • diff --git a/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2ConfigPanel.tsx b/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2ConfigPanel.tsx index 8ff83b6f..cd93a3b5 100644 --- a/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2ConfigPanel.tsx +++ b/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2ConfigPanel.tsx @@ -87,6 +87,10 @@ export const SplitPanelLayout2ConfigPanel: React.FC updateConfig("leftPanel.showEditButton", checked)} />
+ {/* ์ˆ˜์ • ๋ฒ„ํŠผ์ด ์ผœ์ ธ ์žˆ์„ ๋•Œ ๋ชจ๋‹ฌ ํ™”๋ฉด ์„ ํƒ */} + {config.leftPanel?.showEditButton && ( +
+ + updateConfig("leftPanel.editModalScreenId", value)} + placeholder="์ˆ˜์ • ๋ชจ๋‹ฌ ํ™”๋ฉด ์„ ํƒ" + open={leftEditModalOpen} + onOpenChange={setLeftEditModalOpen} + /> +
+ )}
updateConfig("rightPanel.showEditButton", checked)} />
+ {/* ์ˆ˜์ • ๋ฒ„ํŠผ์ด ์ผœ์ ธ ์žˆ์„ ๋•Œ ๋ชจ๋‹ฌ ํ™”๋ฉด ์„ ํƒ */} + {config.rightPanel?.showEditButton && ( +
+ + updateConfig("rightPanel.editModalScreenId", value)} + placeholder="์ˆ˜์ • ๋ชจ๋‹ฌ ํ™”๋ฉด ์„ ํƒ" + open={rightEditModalOpen} + onOpenChange={setRightEditModalOpen} + /> +
+ )}