diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 8510d627..dffbd75b 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -18,10 +18,11 @@ import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRendere import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext"; import { useAuth } from "@/hooks/useAuth"; // ๐Ÿ†• ์‚ฌ์šฉ์ž ์ •๋ณด import { useResponsive } from "@/lib/hooks/useResponsive"; // ๐Ÿ†• ๋ฐ˜์‘ํ˜• ๊ฐ์ง€ -import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; // ๐Ÿ†• ํ…Œ์ด๋ธ” ์˜ต์…˜ -import { TableSearchWidgetHeightProvider, useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext"; // ๐Ÿ†• ๋†’์ด ๊ด€๋ฆฌ -import { ScreenContextProvider } from "@/contexts/ScreenContext"; // ๐Ÿ†• ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ํ†ต์‹  -import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext"; // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋ฆฌ์‚ฌ์ด์ฆˆ +import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; // ํ…Œ์ด๋ธ” ์˜ต์…˜ +import { TableSearchWidgetHeightProvider, useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext"; // ๋†’์ด ๊ด€๋ฆฌ +import { ScreenContextProvider } from "@/contexts/ScreenContext"; // ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ํ†ต์‹  +import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext"; // ๋ถ„ํ•  ํŒจ๋„ ๋ฆฌ์‚ฌ์ด์ฆˆ +import { ActiveTabProvider } from "@/contexts/ActiveTabContext"; // ํ™œ์„ฑ ํƒญ ๊ด€๋ฆฌ function ScreenViewPage() { const params = useParams(); @@ -307,7 +308,8 @@ function ScreenViewPage() { return ( - + +
{/* ๋ ˆ์ด์•„์›ƒ ์ค€๋น„ ์ค‘ ๋กœ๋”ฉ ํ‘œ์‹œ */} {!layoutReady && ( @@ -786,7 +788,8 @@ function ScreenViewPage() { }} />
-
+
+
); } diff --git a/frontend/components/common/ExcelUploadModal.tsx b/frontend/components/common/ExcelUploadModal.tsx index 5f021daa..a4a17274 100644 --- a/frontend/components/common/ExcelUploadModal.tsx +++ b/frontend/components/common/ExcelUploadModal.tsx @@ -29,7 +29,6 @@ import { Plus, Minus, ArrowRight, - Save, Zap, } from "lucide-react"; import { importFromExcel, getExcelSheetNames } from "@/lib/utils/excelExport"; @@ -52,12 +51,6 @@ interface ColumnMapping { systemColumn: string | null; } -interface UploadConfig { - name: string; - type: string; - mappings: ColumnMapping[]; -} - export const ExcelUploadModal: React.FC = ({ open, onOpenChange, @@ -88,8 +81,6 @@ export const ExcelUploadModal: React.FC = ({ const [excelColumns, setExcelColumns] = useState([]); const [systemColumns, setSystemColumns] = useState([]); const [columnMappings, setColumnMappings] = useState([]); - const [configName, setConfigName] = useState(""); - const [configType, setConfigType] = useState(""); // 4๋‹จ๊ณ„: ํ™•์ธ const [isUploading, setIsUploading] = useState(false); @@ -114,7 +105,7 @@ export const ExcelUploadModal: React.FC = ({ const data = await importFromExcel(selectedFile, sheets[0]); setAllData(data); - setDisplayData(data.slice(0, 10)); + setDisplayData(data); // ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ์— ํ‘œ์‹œ (์Šคํฌ๋กค ๊ฐ€๋Šฅ) if (data.length > 0) { const columns = Object.keys(data[0]); @@ -139,7 +130,7 @@ export const ExcelUploadModal: React.FC = ({ try { const data = await importFromExcel(file, sheetName); setAllData(data); - setDisplayData(data.slice(0, 10)); + setDisplayData(data); // ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ์— ํ‘œ์‹œ (์Šคํฌ๋กค ๊ฐ€๋Šฅ) if (data.length > 0) { const columns = Object.keys(data[0]); @@ -275,28 +266,6 @@ export const ExcelUploadModal: React.FC = ({ ); }; - // ์„ค์ • ์ €์žฅ - const handleSaveConfig = () => { - if (!configName.trim()) { - toast.error("๊ฑฐ๋ž˜์ฒ˜๋ช…์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."); - return; - } - - const config: UploadConfig = { - name: configName, - type: configType, - mappings: columnMappings, - }; - - const savedConfigs = JSON.parse( - localStorage.getItem("excelUploadConfigs") || "[]" - ); - savedConfigs.push(config); - localStorage.setItem("excelUploadConfigs", JSON.stringify(savedConfigs)); - - toast.success("์„ค์ •์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); - }; - // ๋‹ค์Œ ๋‹จ๊ณ„ const handleNext = () => { if (currentStep === 1 && !file) { @@ -327,7 +296,8 @@ export const ExcelUploadModal: React.FC = ({ setIsUploading(true); try { - const mappedData = displayData.map((row) => { + // allData๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ „์ฒด ๋ฐ์ดํ„ฐ ์—…๋กœ๋“œ (displayData๋Š” ๋ฏธ๋ฆฌ๋ณด๊ธฐ์šฉ 10๊ฐœ๋งŒ) + const mappedData = allData.map((row) => { const mappedRow: Record = {}; columnMappings.forEach((mapping) => { if (mapping.systemColumn) { @@ -389,8 +359,6 @@ export const ExcelUploadModal: React.FC = ({ setExcelColumns([]); setSystemColumns([]); setColumnMappings([]); - setConfigName(""); - setConfigType(""); } }, [open]); @@ -699,27 +667,25 @@ export const ExcelUploadModal: React.FC = ({ )} - {/* 3๋‹จ๊ณ„: ์ปฌ๋Ÿผ ๋งคํ•‘ - 3๋‹จ ๋ ˆ์ด์•„์›ƒ */} + {/* 3๋‹จ๊ณ„: ์ปฌ๋Ÿผ ๋งคํ•‘ */} {currentStep === 3 && ( -
- {/* ์™ผ์ชฝ: ์ปฌ๋Ÿผ ๋งคํ•‘ ์„ค์ • ์ œ๋ชฉ + ์ž๋™ ๋งคํ•‘ ๋ฒ„ํŠผ */} -
-
-

์ปฌ๋Ÿผ ๋งคํ•‘ ์„ค์ •

- -
+
+ {/* ์ƒ๋‹จ: ์ œ๋ชฉ + ์ž๋™ ๋งคํ•‘ ๋ฒ„ํŠผ */} +
+

์ปฌ๋Ÿผ ๋งคํ•‘ ์„ค์ •

+
- {/* ์ค‘์•™: ๋งคํ•‘ ๋ฆฌ์ŠคํŠธ */} + {/* ๋งคํ•‘ ๋ฆฌ์ŠคํŠธ */}
์—‘์…€ ์ปฌ๋Ÿผ
@@ -772,50 +738,6 @@ export const ExcelUploadModal: React.FC = ({ ))}
- - {/* ์˜ค๋ฅธ์ชฝ: ํ˜„์žฌ ์„ค์ • ์ €์žฅ */} -
-
- -

ํ˜„์žฌ ์„ค์ • ์ €์žฅ

-
-
-
- - setConfigName(e.target.value)} - placeholder="๊ฑฐ๋ž˜์ฒ˜ ์„ ํƒ" - className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" - /> -
-
- - setConfigType(e.target.value)} - placeholder="์œ ํ˜•์„ ์ž…๋ ฅํ•˜์„ธ์š” (์˜ˆ: ์›์ž์žฌ)" - className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" - /> -
- -
-
)} @@ -832,7 +754,7 @@ export const ExcelUploadModal: React.FC = ({ ์‹œํŠธ: {selectedSheet}

- ๋ฐ์ดํ„ฐ ํ–‰: {displayData.length}๊ฐœ + ๋ฐ์ดํ„ฐ ํ–‰: {allData.length}๊ฐœ

ํ…Œ์ด๋ธ”: {tableName} diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 811249a7..cceadae9 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -12,6 +12,7 @@ import { useAuth } from "@/hooks/useAuth"; import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext"; import { useSplitPanelContext } from "@/contexts/SplitPanelContext"; +import { ActiveTabProvider } from "@/contexts/ActiveTabContext"; interface ScreenModalState { isOpen: boolean; @@ -666,6 +667,7 @@ export const ScreenModal: React.FC = ({ className }) => {

) : screenData ? ( +
= ({ className }) => { })}
+
) : (

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

diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index 480b3ddd..376f9953 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -51,6 +51,7 @@ import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; import { TableOptionsToolbar } from "./table-options/TableOptionsToolbar"; import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext"; +import { ActiveTabProvider } from "@/contexts/ActiveTabContext"; /** * ๐Ÿ”— ์—ฐ์‡„ ๋“œ๋กญ๋‹ค์šด ๋ž˜ํผ ์ปดํฌ๋„ŒํŠธ @@ -2103,7 +2104,8 @@ export const InteractiveScreenViewer: React.FC = ( return ( - + +
{/* ํ…Œ์ด๋ธ” ์˜ต์…˜ ํˆด๋ฐ” */} @@ -2210,7 +2212,8 @@ export const InteractiveScreenViewer: React.FC = (
-
+
+
); }; diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index 5b09b092..4763507e 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -39,22 +39,25 @@ interface InteractiveScreenViewerProps { id: number; tableName?: string; }; - menuObjid?: number; // ๐Ÿ†• ๋ฉ”๋‰ด OBJID (์ฝ”๋“œ ์Šค์ฝ”ํ”„์šฉ) + menuObjid?: number; // ๋ฉ”๋‰ด OBJID (์ฝ”๋“œ ์Šค์ฝ”ํ”„์šฉ) onSave?: () => Promise; onRefresh?: () => void; onFlowRefresh?: () => void; - // ๐Ÿ†• ์™ธ๋ถ€์—์„œ ์ „๋‹ฌ๋ฐ›๋Š” ์‚ฌ์šฉ์ž ์ •๋ณด (ScreenModal ๋“ฑ์—์„œ ์‚ฌ์šฉ) + // ์™ธ๋ถ€์—์„œ ์ „๋‹ฌ๋ฐ›๋Š” ์‚ฌ์šฉ์ž ์ •๋ณด (ScreenModal ๋“ฑ์—์„œ ์‚ฌ์šฉ) userId?: string; userName?: string; companyCode?: string; - // ๐Ÿ†• ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ (EditModal์—์„œ ์ „๋‹ฌ) + // ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ (EditModal์—์„œ ์ „๋‹ฌ) groupedData?: Record[]; - // ๐Ÿ†• ๋น„ํ™œ์„ฑํ™”ํ•  ํ•„๋“œ ๋ชฉ๋ก (EditModal์—์„œ ์ „๋‹ฌ) + // ๋น„ํ™œ์„ฑํ™”ํ•  ํ•„๋“œ ๋ชฉ๋ก (EditModal์—์„œ ์ „๋‹ฌ) disabledFields?: string[]; - // ๐Ÿ†• EditModal ๋‚ด๋ถ€์ธ์ง€ ์—ฌ๋ถ€ (button-primary๊ฐ€ EditModal์˜ handleSave ์‚ฌ์šฉํ•˜๋„๋ก) + // EditModal ๋‚ด๋ถ€์ธ์ง€ ์—ฌ๋ถ€ (button-primary๊ฐ€ EditModal์˜ handleSave ์‚ฌ์šฉํ•˜๋„๋ก) isInModal?: boolean; - // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ (์ˆ˜์ • ๋ชจ๋“œ์—์„œ UPDATE ํŒ๋‹จ์šฉ) + // ์›๋ณธ ๋ฐ์ดํ„ฐ (์ˆ˜์ • ๋ชจ๋“œ์—์„œ UPDATE ํŒ๋‹จ์šฉ) originalData?: Record | null; + // ํƒญ ๊ด€๋ จ ์ •๋ณด (ํƒญ ๋‚ด๋ถ€์˜ ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ) + parentTabId?: string; // ๋ถ€๋ชจ ํƒญ ID + parentTabsComponentId?: string; // ๋ถ€๋ชจ ํƒญ ์ปดํฌ๋„ŒํŠธ ID } export const InteractiveScreenViewerDynamic: React.FC = ({ @@ -74,7 +77,9 @@ export const InteractiveScreenViewerDynamic: React.FC { const { isPreviewMode } = useScreenPreview(); // ํ”„๋ฆฌ๋ทฐ ๋ชจ๋“œ ํ™•์ธ const { userName: authUserName, user: authUser } = useAuth(); @@ -359,43 +364,43 @@ export const InteractiveScreenViewerDynamic: React.FC { - console.log("๐Ÿ” ํ…Œ์ด๋ธ”์—์„œ ์„ ํƒ๋œ ํ–‰ ๋ฐ์ดํ„ฐ:", selectedData); + console.log("ํ…Œ์ด๋ธ”์—์„œ ์„ ํƒ๋œ ํ–‰ ๋ฐ์ดํ„ฐ:", selectedData); setSelectedRowsData(selectedData); }} - // ๐Ÿ†• ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ์ „๋‹ฌ (EditModal โ†’ ModalRepeaterTable) groupedData={groupedData} - // ๐Ÿ†• ๋น„ํ™œ์„ฑํ™” ํ•„๋“œ ์ „๋‹ฌ (EditModal โ†’ ๊ฐ ์ปดํฌ๋„ŒํŠธ) disabledFields={disabledFields} flowSelectedData={flowSelectedData} flowSelectedStepId={flowSelectedStepId} onFlowSelectedDataChange={(selectedData, stepId) => { - console.log("๐Ÿ” ํ”Œ๋กœ์šฐ์—์„œ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ:", { selectedData, stepId }); + console.log("ํ”Œ๋กœ์šฐ์—์„œ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ:", { selectedData, stepId }); setFlowSelectedData(selectedData); setFlowSelectedStepId(stepId); }} onRefresh={ onRefresh || (() => { - // ๋ถ€๋ชจ๋กœ๋ถ€ํ„ฐ ์ „๋‹ฌ๋ฐ›์€ onRefresh ๋˜๋Š” ๊ธฐ๋ณธ ๋™์ž‘ - console.log("๐Ÿ”„ InteractiveScreenViewerDynamic onRefresh ํ˜ธ์ถœ"); + console.log("InteractiveScreenViewerDynamic onRefresh ํ˜ธ์ถœ"); }) } onFlowRefresh={onFlowRefresh} onClose={() => { // buttonActions.ts๊ฐ€ ์ด๋ฏธ ์ฒ˜๋ฆฌํ•จ }} + // ํƒญ ๊ด€๋ จ ์ •๋ณด ์ „๋‹ฌ + parentTabId={parentTabId} + parentTabsComponentId={parentTabsComponentId} /> ); } diff --git a/frontend/components/screen/widgets/TabsWidget.tsx b/frontend/components/screen/widgets/TabsWidget.tsx index 7990a2a6..4d8147c9 100644 --- a/frontend/components/screen/widgets/TabsWidget.tsx +++ b/frontend/components/screen/widgets/TabsWidget.tsx @@ -7,15 +7,18 @@ import { X, Loader2 } from "lucide-react"; import type { TabsComponent, TabItem } from "@/types/screen-management"; import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic"; import { cn } from "@/lib/utils"; +import { useActiveTab } from "@/contexts/ActiveTabContext"; interface TabsWidgetProps { component: TabsComponent; className?: string; style?: React.CSSProperties; - menuObjid?: number; // ๐Ÿ†• ๋ถ€๋ชจ ํ™”๋ฉด์˜ ๋ฉ”๋‰ด OBJID + menuObjid?: number; // ๋ถ€๋ชจ ํ™”๋ฉด์˜ ๋ฉ”๋‰ด OBJID } export function TabsWidget({ component, className, style, menuObjid }: TabsWidgetProps) { + // ActiveTab context ์‚ฌ์šฉ + const { setActiveTab, removeTabsComponent } = useActiveTab(); const { tabs = [], defaultTab, @@ -51,12 +54,30 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge setVisibleTabs(tabs.filter((tab) => !tab.disabled)); }, [tabs]); - // ์„ ํƒ๋œ ํƒญ ๋ณ€๊ฒฝ ์‹œ localStorage์— ์ €์žฅ + // ์„ ํƒ๋œ ํƒญ ๋ณ€๊ฒฝ ์‹œ localStorage์— ์ €์žฅ + ActiveTab Context ์—…๋ฐ์ดํŠธ useEffect(() => { if (persistSelection && typeof window !== "undefined") { localStorage.setItem(storageKey, selectedTab); } - }, [selectedTab, persistSelection, storageKey]); + + // ActiveTab Context์— ํ˜„์žฌ ํ™œ์„ฑ ํƒญ ์ •๋ณด ๋“ฑ๋ก + const currentTabInfo = visibleTabs.find(t => t.id === selectedTab); + if (currentTabInfo) { + setActiveTab(component.id, { + tabId: selectedTab, + tabsComponentId: component.id, + screenId: currentTabInfo.screenId, + label: currentTabInfo.label, + }); + } + }, [selectedTab, persistSelection, storageKey, component.id, visibleTabs, setActiveTab]); + + // ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ ActiveTab Context์—์„œ ์ œ๊ฑฐ + useEffect(() => { + return () => { + removeTabsComponent(component.id); + }; + }, [component.id, removeTabsComponent]); // ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ ์„ ํƒ๋œ ํƒญ์˜ ํ™”๋ฉด ๋ถˆ๋Ÿฌ์˜ค๊ธฐ useEffect(() => { @@ -220,16 +241,18 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge margin: "0 auto", }} > - {components.map((component: any) => ( + {components.map((comp: any) => ( ))}
diff --git a/frontend/contexts/ActiveTabContext.tsx b/frontend/contexts/ActiveTabContext.tsx new file mode 100644 index 00000000..75b25fbb --- /dev/null +++ b/frontend/contexts/ActiveTabContext.tsx @@ -0,0 +1,139 @@ +"use client"; + +import React, { + createContext, + useContext, + useState, + useCallback, + ReactNode, +} from "react"; + +/** + * ํ™œ์„ฑ ํƒญ ์ •๋ณด + */ +export interface ActiveTabInfo { + tabId: string; // ํƒญ ๊ณ ์œ  ID + tabsComponentId: string; // ๋ถ€๋ชจ ํƒญ ์ปดํฌ๋„ŒํŠธ ID + screenId?: number; // ํƒญ์— ์—ฐ๊ฒฐ๋œ ํ™”๋ฉด ID + label?: string; // ํƒญ ๋ผ๋ฒจ +} + +/** + * Context ๊ฐ’ ํƒ€์ž… + */ +interface ActiveTabContextValue { + // ํ˜„์žฌ ํ™œ์„ฑ ํƒญ ์ •๋ณด (ํƒญ ์ปดํฌ๋„ŒํŠธ ID -> ํ™œ์„ฑ ํƒญ ์ •๋ณด) + activeTabs: Map; + + // ํ™œ์„ฑ ํƒญ ์„ค์ • + setActiveTab: (tabsComponentId: string, tabInfo: ActiveTabInfo) => void; + + // ํ™œ์„ฑ ํƒญ ์กฐํšŒ + getActiveTab: (tabsComponentId: string) => ActiveTabInfo | undefined; + + // ํŠน์ • ํƒญ ์ปดํฌ๋„ŒํŠธ์˜ ํ™œ์„ฑ ํƒญ ID ์กฐํšŒ + getActiveTabId: (tabsComponentId: string) => string | undefined; + + // ์ „์ฒด ํ™œ์„ฑ ํƒญ ID ๋ชฉ๋ก (๋ชจ๋“  ํƒญ ์ปดํฌ๋„ŒํŠธ์—์„œ) + getAllActiveTabIds: () => string[]; + + // ํƒญ ์ปดํฌ๋„ŒํŠธ ์ œ๊ฑฐ ์‹œ ์ •๋ฆฌ + removeTabsComponent: (tabsComponentId: string) => void; +} + +const ActiveTabContext = createContext(undefined); + +export const ActiveTabProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [activeTabs, setActiveTabs] = useState>(new Map()); + + /** + * ํ™œ์„ฑ ํƒญ ์„ค์ • + */ + const setActiveTab = useCallback((tabsComponentId: string, tabInfo: ActiveTabInfo) => { + setActiveTabs((prev) => { + const newMap = new Map(prev); + newMap.set(tabsComponentId, tabInfo); + return newMap; + }); + }, []); + + /** + * ํ™œ์„ฑ ํƒญ ์กฐํšŒ + */ + const getActiveTab = useCallback( + (tabsComponentId: string) => { + return activeTabs.get(tabsComponentId); + }, + [activeTabs] + ); + + /** + * ํŠน์ • ํƒญ ์ปดํฌ๋„ŒํŠธ์˜ ํ™œ์„ฑ ํƒญ ID ์กฐํšŒ + */ + const getActiveTabId = useCallback( + (tabsComponentId: string) => { + return activeTabs.get(tabsComponentId)?.tabId; + }, + [activeTabs] + ); + + /** + * ์ „์ฒด ํ™œ์„ฑ ํƒญ ID ๋ชฉ๋ก + */ + const getAllActiveTabIds = useCallback(() => { + return Array.from(activeTabs.values()).map((info) => info.tabId); + }, [activeTabs]); + + /** + * ํƒญ ์ปดํฌ๋„ŒํŠธ ์ œ๊ฑฐ ์‹œ ์ •๋ฆฌ + */ + const removeTabsComponent = useCallback((tabsComponentId: string) => { + setActiveTabs((prev) => { + const newMap = new Map(prev); + newMap.delete(tabsComponentId); + return newMap; + }); + }, []); + + return ( + + {children} + + ); +}; + +/** + * Context Hook + */ +export const useActiveTab = () => { + const context = useContext(ActiveTabContext); + if (!context) { + // Context๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜ (ํƒญ์ด ์—†๋Š” ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ ์‹œ) + return { + activeTabs: new Map(), + setActiveTab: () => {}, + getActiveTab: () => undefined, + getActiveTabId: () => undefined, + getAllActiveTabIds: () => [], + removeTabsComponent: () => {}, + }; + } + return context; +}; + +/** + * Optional Context Hook (์—๋Ÿฌ ์—†์ด undefined ๋ฐ˜ํ™˜) + */ +export const useActiveTabOptional = () => { + return useContext(ActiveTabContext); +}; + diff --git a/frontend/contexts/TableOptionsContext.tsx b/frontend/contexts/TableOptionsContext.tsx index d706443f..1e991201 100644 --- a/frontend/contexts/TableOptionsContext.tsx +++ b/frontend/contexts/TableOptionsContext.tsx @@ -3,12 +3,14 @@ import React, { useContext, useState, useCallback, + useMemo, ReactNode, } from "react"; import { TableRegistration, TableOptionsContextValue, } from "@/types/table-options"; +import { useActiveTab } from "./ActiveTabContext"; const TableOptionsContext = createContext( undefined @@ -89,6 +91,35 @@ export const TableOptionsProvider: React.FC<{ children: ReactNode }> = ({ }); }, []); + // ActiveTab context ์‚ฌ์šฉ (optional - ์—๋Ÿฌ ๋ฐฉ์ง€) + const activeTabContext = useActiveTab(); + + /** + * ํ˜„์žฌ ํ™œ์„ฑ ํƒญ์˜ ํ…Œ์ด๋ธ”๋งŒ ๋ฐ˜ํ™˜ + */ + const getActiveTabTables = useCallback(() => { + const allTables = Array.from(registeredTables.values()); + const activeTabIds = activeTabContext.getAllActiveTabIds(); + + // ํ™œ์„ฑ ํƒญ์ด ์—†์œผ๋ฉด ํƒญ์— ์†ํ•˜์ง€ ์•Š์€ ํ…Œ์ด๋ธ”๋งŒ ๋ฐ˜ํ™˜ + if (activeTabIds.length === 0) { + return allTables.filter(table => !table.parentTabId); + } + + // ํ™œ์„ฑ ํƒญ์— ์†ํ•œ ํ…Œ์ด๋ธ” + ํƒญ์— ์†ํ•˜์ง€ ์•Š์€ ํ…Œ์ด๋ธ” + return allTables.filter(table => + !table.parentTabId || activeTabIds.includes(table.parentTabId) + ); + }, [registeredTables, activeTabContext]); + + /** + * ํŠน์ • ํƒญ์˜ ํ…Œ์ด๋ธ”๋งŒ ๋ฐ˜ํ™˜ + */ + const getTablesForTab = useCallback((tabId: string) => { + const allTables = Array.from(registeredTables.values()); + return allTables.filter(table => table.parentTabId === tabId); + }, [registeredTables]); + return ( = ({ updateTableDataCount, selectedTableId, setSelectedTableId, + getActiveTabTables, + getTablesForTab, }} > {children} diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 6ca5e68f..0c62cd8b 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -132,6 +132,9 @@ export interface DynamicComponentRendererProps { mode?: "view" | "edit"; // ๋ชจ๋‹ฌ ๋‚ด์—์„œ ๋ Œ๋”๋ง ์—ฌ๋ถ€ isInModal?: boolean; + // ํƒญ ๊ด€๋ จ ์ •๋ณด (ํƒญ ๋‚ด๋ถ€์˜ ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ) + parentTabId?: string; // ๋ถ€๋ชจ ํƒญ ID + parentTabsComponentId?: string; // ๋ถ€๋ชจ ํƒญ ์ปดํฌ๋„ŒํŠธ ID [key: string]: any; } diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index d3bb3d5a..bed2b795 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -208,6 +208,9 @@ export interface TableListComponentProps { ) => void; onConfigChange?: (config: any) => void; refreshKey?: number; + // ํƒญ ๊ด€๋ จ ์ •๋ณด (ํƒญ ๋‚ด๋ถ€์˜ ํ…Œ์ด๋ธ”์—์„œ ์‚ฌ์šฉ) + parentTabId?: string; // ๋ถ€๋ชจ ํƒญ ID + parentTabsComponentId?: string; // ๋ถ€๋ชจ ํƒญ ์ปดํฌ๋„ŒํŠธ ID } // ======================================== @@ -224,7 +227,7 @@ export const TableListComponent: React.FC = ({ config, className, style, - formData: propFormData, // ๐Ÿ†• ๋ถ€๋ชจ์—์„œ ์ „๋‹ฌ๋ฐ›์€ formData + formData: propFormData, onFormDataChange, componentConfig, onSelectedRowsChange, @@ -232,7 +235,9 @@ export const TableListComponent: React.FC = ({ refreshKey, tableName, userId, - screenId, // ํ™”๋ฉด ID ์ถ”์ถœ + screenId, + parentTabId, + parentTabsComponentId, }) => { // ======================================== // ์„ค์ • ๋ฐ ์Šคํƒ€์ผ @@ -1016,7 +1021,11 @@ export const TableListComponent: React.FC = ({ onGroupChange: setGrouping, onColumnVisibilityChange: setColumnVisibility, getColumnUniqueValues, // ๊ณ ์œ  ๊ฐ’ ์กฐํšŒ ํ•จ์ˆ˜ ๋“ฑ๋ก - onGroupSumChange: setGroupSumConfig, // ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ ์„ค์ • + onGroupSumChange: setGroupSumConfig, // ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ ์„ค์ • + // ํƒญ ๊ด€๋ จ ์ •๋ณด (ํƒญ ๋‚ด๋ถ€์˜ ํ…Œ์ด๋ธ”์ธ ๊ฒฝ์šฐ) + parentTabId, + parentTabsComponentId, + screenId: screenId ? Number(screenId) : undefined, }; registerTable(registration); diff --git a/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx b/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx index 0dde5ea9..e486338a 100644 --- a/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx +++ b/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx @@ -6,6 +6,7 @@ import { Input } from "@/components/ui/input"; import { Settings, Filter, Layers, X, Check, ChevronsUpDown } from "lucide-react"; import { useTableOptions } from "@/contexts/TableOptionsContext"; import { useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext"; +import { useActiveTab } from "@/contexts/ActiveTabContext"; import { ColumnVisibilityPanel } from "@/components/screen/table-options/ColumnVisibilityPanel"; import { FilterPanel } from "@/components/screen/table-options/FilterPanel"; import { GroupingPanel } from "@/components/screen/table-options/GroupingPanel"; @@ -49,8 +50,9 @@ interface TableSearchWidgetProps { } export function TableSearchWidget({ component, screenId, onHeightChange }: TableSearchWidgetProps) { - const { registeredTables, selectedTableId, setSelectedTableId, getTable } = useTableOptions(); + const { registeredTables, selectedTableId, setSelectedTableId, getTable, getActiveTabTables } = useTableOptions(); const { isPreviewMode } = useScreenPreview(); // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ชจ๋“œ ํ™•์ธ + const { getAllActiveTabIds, activeTabs } = useActiveTab(); // ํ™œ์„ฑ ํƒญ ์ •๋ณด // ๋†’์ด ๊ด€๋ฆฌ context (์‹ค์ œ ํ™”๋ฉด์—์„œ๋งŒ ์‚ฌ์šฉ) let setWidgetHeight: @@ -63,6 +65,9 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table // Context๊ฐ€ ์—†์œผ๋ฉด (๋””์ž์ด๋„ˆ ๋ชจ๋“œ) ๋ฌด์‹œ setWidgetHeight = undefined; } + + // ํƒญ๋ณ„ ํ•„ํ„ฐ ๊ฐ’ ์ €์žฅ (ํƒญ ID -> ํ•„ํ„ฐ ๊ฐ’) + const [tabFilterValues, setTabFilterValues] = useState>>({}); const [columnVisibilityOpen, setColumnVisibilityOpen] = useState(false); const [filterOpen, setFilterOpen] = useState(false); @@ -88,38 +93,48 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table // Map์„ ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ const allTableList = Array.from(registeredTables.values()); - // ๋Œ€์ƒ ํŒจ๋„ ์œ„์น˜์— ๋”ฐ๋ผ ํ…Œ์ด๋ธ” ํ•„ํ„ฐ๋ง (tableId ํŒจํ„ด ๊ธฐ๋ฐ˜) + // ํ˜„์žฌ ํ™œ์„ฑ ํƒญ ID ๋ชฉ๋ก + const activeTabIds = useMemo(() => getAllActiveTabIds(), [activeTabs]); + + // ๋Œ€์ƒ ํŒจ๋„ ์œ„์น˜ + ํ™œ์„ฑ ํƒญ์— ๋”ฐ๋ผ ํ…Œ์ด๋ธ” ํ•„ํ„ฐ๋ง const tableList = useMemo(() => { - // "auto"๋ฉด ๋ชจ๋“  ํ…Œ์ด๋ธ” ๋ฐ˜ํ™˜ - if (targetPanelPosition === "auto") { - return allTableList; - } - - // ํ…Œ์ด๋ธ” ID ํŒจํ„ด์œผ๋กœ ํ•„ํ„ฐ๋ง - // card-display-XXX: ์ขŒ์ธก ํŒจ๋„ (์นด๋“œ ๋””์Šคํ”Œ๋ ˆ์ด) - // datatable-XXX, table-list-XXX: ์šฐ์ธก ํŒจ๋„ (ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ) - const filteredTables = allTableList.filter(table => { - const tableId = table.tableId.toLowerCase(); - - if (targetPanelPosition === "left") { - // ์ขŒ์ธก ํŒจ๋„ ๋Œ€์ƒ: card-display๋งŒ - return tableId.includes("card-display") || tableId.includes("card"); - } else if (targetPanelPosition === "right") { - // ์šฐ์ธก ํŒจ๋„ ๋Œ€์ƒ: datatable, table-list ๋“ฑ (card-display ์ œ์™ธ) - const isCardDisplay = tableId.includes("card-display") || tableId.includes("card"); - return !isCardDisplay; - } - - return true; + // 1๋‹จ๊ณ„: ํ™œ์„ฑ ํƒญ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง + // - ํ™œ์„ฑ ํƒญ์— ์†ํ•œ ํ…Œ์ด๋ธ”๋งŒ ํ‘œ์‹œ + // - ํƒญ์— ์†ํ•˜์ง€ ์•Š์€ ํ…Œ์ด๋ธ”(parentTabId๊ฐ€ ์—†๋Š”)๋„ ํฌํ•จ + let filteredByTab = allTableList.filter(table => { + // ํƒญ์— ์†ํ•˜์ง€ ์•Š๋Š” ํ…Œ์ด๋ธ”์€ ํ•ญ์ƒ ํ‘œ์‹œ + if (!table.parentTabId) return true; + // ํ™œ์„ฑ ํƒญ์— ์†ํ•œ ํ…Œ์ด๋ธ”๋งŒ ํ‘œ์‹œ + return activeTabIds.includes(table.parentTabId); }); - // ํ•„ํ„ฐ๋ง๋œ ๊ฒฐ๊ณผ๊ฐ€ ์—†์œผ๋ฉด ๋ชจ๋“  ํ…Œ์ด๋ธ” ๋ฐ˜ํ™˜ (ํด๋ฐฑ) - if (filteredTables.length === 0) { - return allTableList; + // 2๋‹จ๊ณ„: ๋Œ€์ƒ ํŒจ๋„ ์œ„์น˜์— ๋”ฐ๋ผ ์ถ”๊ฐ€ ํ•„ํ„ฐ๋ง + if (targetPanelPosition !== "auto") { + filteredByTab = filteredByTab.filter(table => { + const tableId = table.tableId.toLowerCase(); + + if (targetPanelPosition === "left") { + // ์ขŒ์ธก ํŒจ๋„ ๋Œ€์ƒ: card-display๋งŒ + return tableId.includes("card-display") || tableId.includes("card"); + } else if (targetPanelPosition === "right") { + // ์šฐ์ธก ํŒจ๋„ ๋Œ€์ƒ: datatable, table-list ๋“ฑ (card-display ์ œ์™ธ) + const isCardDisplay = tableId.includes("card-display") || tableId.includes("card"); + return !isCardDisplay; + } + + return true; + }); } - return filteredTables; - }, [allTableList, targetPanelPosition]); + // ํ•„ํ„ฐ๋ง๋œ ๊ฒฐ๊ณผ๊ฐ€ ์—†์œผ๋ฉด ํƒญ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง ๊ฒฐ๊ณผ๋งŒ ๋ฐ˜ํ™˜ + if (filteredByTab.length === 0) { + return allTableList.filter(table => + !table.parentTabId || activeTabIds.includes(table.parentTabId) + ); + } + + return filteredByTab; + }, [allTableList, targetPanelPosition, activeTabIds]); // currentTable์€ tableList(ํ•„ํ„ฐ๋ง๋œ ๋ชฉ๋ก)์—์„œ ๊ฐ€์ ธ์™€์•ผ ํ•จ const currentTable = useMemo(() => { @@ -151,6 +166,34 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table } }, [tableList, selectedTableId, autoSelectFirstTable, setSelectedTableId, targetPanelPosition]); + // ํ˜„์žฌ ์„ ํƒ๋œ ํ…Œ์ด๋ธ”์˜ ํƒญ ID (ํƒญ๋ณ„ ํ•„ํ„ฐ ์ €์žฅ์šฉ) + const currentTableTabId = currentTable?.parentTabId; + + // ํƒญ๋ณ„ ํ•„ํ„ฐ ๊ฐ’ ์ €์žฅ ํ‚ค ์ƒ์„ฑ + const getTabFilterStorageKey = (tableName: string, tabId?: string) => { + const baseKey = screenId + ? `table_filter_values_${tableName}_screen_${screenId}` + : `table_filter_values_${tableName}`; + return tabId ? `${baseKey}_tab_${tabId}` : baseKey; + }; + + // ํƒญ ๋ณ€๊ฒฝ ์‹œ ์ด์ „ ํƒญ์˜ ํ•„ํ„ฐ ๊ฐ’ ์ €์žฅ + ์ƒˆ ํƒญ์˜ ํ•„ํ„ฐ ๊ฐ’ ๋ณต์› + useEffect(() => { + if (!currentTable?.tableName) return; + + // ํ˜„์žฌ ํ•„ํ„ฐ ๊ฐ’์ด ์žˆ์œผ๋ฉด ํƒญ๋ณ„๋กœ ์ €์žฅ + if (Object.keys(filterValues).length > 0 && currentTableTabId) { + const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId); + localStorage.setItem(storageKey, JSON.stringify(filterValues)); + + // ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ์—๋„ ์ €์žฅ + setTabFilterValues(prev => ({ + ...prev, + [currentTableTabId]: filterValues + })); + } + }, [currentTableTabId, currentTable?.tableName]); + // ํ˜„์žฌ ํ…Œ์ด๋ธ”์˜ ์ €์žฅ๋œ ํ•„ํ„ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ (๋™์  ๋ชจ๋“œ) ๋˜๋Š” ๊ณ ์ • ํ•„ํ„ฐ ์ ์šฉ (๊ณ ์ • ๋ชจ๋“œ) useEffect(() => { if (!currentTable?.tableName) return; @@ -165,14 +208,32 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table width: f.width || 200, })); setActiveFilters(activeFiltersList); + + // ํƒญ๋ณ„ ์ €์žฅ๋œ ํ•„ํ„ฐ ๊ฐ’ ๋ณต์› + if (currentTableTabId) { + const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId); + const savedValues = localStorage.getItem(storageKey); + if (savedValues) { + try { + const parsedValues = JSON.parse(savedValues); + setFilterValues(parsedValues); + // ์ฆ‰์‹œ ํ•„ํ„ฐ ์ ์šฉ + setTimeout(() => applyFilters(parsedValues), 100); + } catch { + setFilterValues({}); + } + } else { + setFilterValues({}); + } + } return; } - // ๋™์  ๋ชจ๋“œ: ํ™”๋ฉด๋ณ„๋กœ ๋…๋ฆฝ์ ์ธ ํ•„ํ„ฐ ์„ค์ • ๋ถˆ๋Ÿฌ์˜ค๊ธฐ - const storageKey = screenId - ? `table_filters_${currentTable.tableName}_screen_${screenId}` + // ๋™์  ๋ชจ๋“œ: ํ™”๋ฉด๋ณ„ + ํƒญ๋ณ„๋กœ ๋…๋ฆฝ์ ์ธ ํ•„ํ„ฐ ์„ค์ • ๋ถˆ๋Ÿฌ์˜ค๊ธฐ + const filterConfigKey = screenId + ? `table_filters_${currentTable.tableName}_screen_${screenId}${currentTableTabId ? `_tab_${currentTableTabId}` : ''}` : `table_filters_${currentTable.tableName}`; - const savedFilters = localStorage.getItem(storageKey); + const savedFilters = localStorage.getItem(filterConfigKey); if (savedFilters) { try { @@ -193,16 +254,39 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table operator: "contains", value: "", filterType: f.filterType, - width: f.width || 200, // ์ €์žฅ๋œ ๋„ˆ๋น„ ํฌํ•จ + width: f.width || 200, })); setActiveFilters(activeFiltersList); + + // ํƒญ๋ณ„ ์ €์žฅ๋œ ํ•„ํ„ฐ ๊ฐ’ ๋ณต์› + if (currentTableTabId) { + const valuesStorageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId); + const savedValues = localStorage.getItem(valuesStorageKey); + if (savedValues) { + try { + const parsedValues = JSON.parse(savedValues); + setFilterValues(parsedValues); + // ์ฆ‰์‹œ ํ•„ํ„ฐ ์ ์šฉ + setTimeout(() => applyFilters(parsedValues), 100); + } catch { + setFilterValues({}); + } + } else { + setFilterValues({}); + } + } else { + setFilterValues({}); + } } catch (error) { console.error("์ €์žฅ๋œ ํ•„ํ„ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์‹คํŒจ:", error); } + } else { + // ํ•„ํ„ฐ ์„ค์ •์ด ์—†์œผ๋ฉด ์ดˆ๊ธฐํ™” + setFilterValues({}); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentTable?.tableName, filterMode, screenId, JSON.stringify(presetFilters)]); + }, [currentTable?.tableName, filterMode, screenId, currentTableTabId, JSON.stringify(presetFilters)]); // select ์˜ต์…˜ ์ดˆ๊ธฐ ๋กœ๋“œ (ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰, ์ดํ›„ ์œ ์ง€) useEffect(() => { @@ -300,6 +384,12 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table setFilterValues(newValues); + // ํƒญ๋ณ„ ํ•„ํ„ฐ ๊ฐ’ ์ €์žฅ + if (currentTable?.tableName && currentTableTabId) { + const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId); + localStorage.setItem(storageKey, JSON.stringify(newValues)); + } + // ์‹ค์‹œ๊ฐ„ ๊ฒ€์ƒ‰: ๊ฐ’ ๋ณ€๊ฒฝ ์‹œ ์ฆ‰์‹œ ํ•„ํ„ฐ ์ ์šฉ applyFilters(newValues); }; @@ -365,6 +455,12 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table setFilterValues({}); setSelectedLabels({}); currentTable?.onFilterChange([]); + + // ํƒญ๋ณ„ ์ €์žฅ๋œ ํ•„ํ„ฐ ๊ฐ’๋„ ์ดˆ๊ธฐํ™” + if (currentTable?.tableName && currentTableTabId) { + const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId); + localStorage.removeItem(storageKey); + } }; // ํ•„ํ„ฐ ์ž…๋ ฅ ํ•„๋“œ ๋ Œ๋”๋ง diff --git a/frontend/types/table-options.ts b/frontend/types/table-options.ts index 5685f127..3fcdb846 100644 --- a/frontend/types/table-options.ts +++ b/frontend/types/table-options.ts @@ -55,12 +55,17 @@ export interface TableRegistration { tableName: string; // ์‹ค์ œ DB ํ…Œ์ด๋ธ”๋ช… (์˜ˆ: "item_info") columns: TableColumn[]; dataCount?: number; // ํ˜„์žฌ ํ‘œ์‹œ๋œ ๋ฐ์ดํ„ฐ ๊ฑด์ˆ˜ + + // ํƒญ ๊ด€๋ จ ์ •๋ณด (ํƒญ ๋‚ด๋ถ€์— ์žˆ๋Š” ํ…Œ์ด๋ธ”์˜ ๊ฒฝ์šฐ) + parentTabId?: string; // ๋ถ€๋ชจ ํƒญ ID + parentTabsComponentId?: string; // ๋ถ€๋ชจ ํƒญ ์ปดํฌ๋„ŒํŠธ ID + screenId?: number; // ์†Œ์† ํ™”๋ฉด ID // ์ฝœ๋ฐฑ ํ•จ์ˆ˜๋“ค onFilterChange: (filters: TableFilter[]) => void; onGroupChange: (groups: string[]) => void; onColumnVisibilityChange: (columns: ColumnVisibility[]) => void; - onGroupSumChange?: (config: GroupSumConfig | null) => void; // ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ ์„ค์ • ๋ณ€๊ฒฝ + onGroupSumChange?: (config: GroupSumConfig | null) => void; // ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ ์„ค์ • ๋ณ€๊ฒฝ // ๋ฐ์ดํ„ฐ ์กฐํšŒ ํ•จ์ˆ˜ (์„ ํƒ ํƒ€์ž… ํ•„ํ„ฐ์šฉ) getColumnUniqueValues?: (columnName: string) => Promise>; @@ -77,4 +82,8 @@ export interface TableOptionsContextValue { updateTableDataCount: (tableId: string, count: number) => void; // ๋ฐ์ดํ„ฐ ๊ฑด์ˆ˜ ์—…๋ฐ์ดํŠธ selectedTableId: string | null; setSelectedTableId: (tableId: string | null) => void; + + // ํ™œ์„ฑ ํƒญ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง + getActiveTabTables: () => TableRegistration[]; // ํ˜„์žฌ ํ™œ์„ฑ ํƒญ์˜ ํ…Œ์ด๋ธ”๋งŒ ๋ฐ˜ํ™˜ + getTablesForTab: (tabId: string) => TableRegistration[]; // ํŠน์ • ํƒญ์˜ ํ…Œ์ด๋ธ”๋งŒ ๋ฐ˜ํ™˜ }