"use client"; import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode } from "react"; export interface TabItem { id: string; screenId: number; menuObjid?: number; screenName: string; menuName?: string; parentMenuName?: string; isAdminMode?: boolean; } interface TabContextType { tabs: TabItem[]; activeTabId: string | null; refreshKeys: Record; openTab: (tab: Omit, insertIndex?: number) => void; closeTab: (tabId: string) => void; closeOtherTabs: (tabId: string) => void; closeAllTabs: () => void; closeTabsToTheLeft: (tabId: string) => void; closeTabsToTheRight: (tabId: string) => void; switchTab: (tabId: string) => void; getActiveTab: () => TabItem | undefined; updateTabOrder: (newOrder: string[]) => void; refreshTab: (tabId: string) => void; } const TAB_STORAGE_KEY = "erp_open_tabs"; const ACTIVE_TAB_STORAGE_KEY = "erp_active_tab"; const TabContext = createContext(undefined); function generateTabId(screenId: number, menuObjid?: number): string { return `tab-${screenId}-${menuObjid || "default"}`; } function clearTabStateCache(tab: TabItem) { try { // 페이지 레벨 캐시 삭제 sessionStorage.removeItem(`tab-cache-${tab.screenId}-${tab.menuObjid || "default"}`); // 페이지 스크롤 위치 캐시 삭제 sessionStorage.removeItem(`page-scroll-${tab.screenId}-${tab.menuObjid || "default"}`); // 하위 컴포넌트 캐시 삭제 (category-mgr, category-widget) if (tab.menuObjid) { sessionStorage.removeItem(`category-mgr-${tab.menuObjid}`); sessionStorage.removeItem(`category-widget-${tab.menuObjid}`); } sessionStorage.removeItem(`category-mgr-${tab.screenId}`); sessionStorage.removeItem(`category-widget-${tab.screenId}`); // TSP: 하위 컴포넌트 캐시 일괄 삭제 (레거시 + 새 usePersistedState 훅) const prefixes = [ `tsp-${tab.screenId}-`, `tabs-session-${tab.menuObjid || "g"}-`, `table-state-${tab.screenId}-`, `split-sel-${tab.screenId}-`, `catval-sel-${tab.screenId}-`, `category-mgr-${tab.menuObjid || tab.screenId}-`, `bom-tree-${tab.screenId}-`, ]; const keysToRemove: string[] = []; for (let i = 0; i < sessionStorage.length; i++) { const key = sessionStorage.key(i); if (key && prefixes.some(p => key.startsWith(p))) { keysToRemove.push(key); } } keysToRemove.forEach(k => sessionStorage.removeItem(k)); } catch { /* 무시 */ } } function loadTabsFromStorage(): { tabs: TabItem[]; activeTabId: string | null } { if (typeof window === "undefined") return { tabs: [], activeTabId: null }; try { const tabsJson = sessionStorage.getItem(TAB_STORAGE_KEY); const activeTabId = sessionStorage.getItem(ACTIVE_TAB_STORAGE_KEY); const tabs = tabsJson ? JSON.parse(tabsJson) : []; return { tabs, activeTabId }; } catch { return { tabs: [], activeTabId: null }; } } function saveTabsToStorage(tabs: TabItem[], activeTabId: string | null) { if (typeof window === "undefined") return; try { sessionStorage.setItem(TAB_STORAGE_KEY, JSON.stringify(tabs)); if (activeTabId) { sessionStorage.setItem(ACTIVE_TAB_STORAGE_KEY, activeTabId); } else { sessionStorage.removeItem(ACTIVE_TAB_STORAGE_KEY); } } catch { // sessionStorage 용량 초과 등 무시 } } export function TabProvider({ children }: { children: ReactNode }) { const [tabs, setTabs] = useState([]); const [activeTabId, setActiveTabId] = useState(null); const [initialized, setInitialized] = useState(false); const [refreshKeys, setRefreshKeys] = useState>({}); // sessionStorage에서 복원 useEffect(() => { const { tabs: savedTabs, activeTabId: savedActiveTabId } = loadTabsFromStorage(); if (savedTabs.length > 0) { setTabs(savedTabs); const activeId = savedActiveTabId && savedTabs.some(t => t.id === savedActiveTabId) ? savedActiveTabId : savedTabs[0].id; setActiveTabId(activeId); // 활성 탭의 상태 캐시 삭제 (F5 시 활성 탭은 정상 새로고침) const activeTab = savedTabs.find(t => t.id === activeId); if (activeTab) { clearTabStateCache(activeTab); } } setInitialized(true); }, []); // 상태 변경 시 sessionStorage에 저장 useEffect(() => { if (!initialized) return; saveTabsToStorage(tabs, activeTabId); }, [tabs, activeTabId, initialized]); const openTab = useCallback((tabData: Omit, insertIndex?: number) => { const tabId = generateTabId(tabData.screenId, tabData.menuObjid); setTabs((prevTabs) => { const existingTab = prevTabs.find(t => t.screenId === tabData.screenId); if (existingTab) { setActiveTabId(existingTab.id); return prevTabs; } const newTab: TabItem = { ...tabData, id: tabId }; setActiveTabId(tabId); if (insertIndex !== undefined && insertIndex >= 0 && insertIndex <= prevTabs.length) { const newTabs = [...prevTabs]; newTabs.splice(insertIndex, 0, newTab); return newTabs; } return [...prevTabs, newTab]; }); }, []); const closeTab = useCallback((tabId: string) => { setTabs((prevTabs) => { const tabIndex = prevTabs.findIndex(t => t.id === tabId); if (tabIndex === -1) return prevTabs; clearTabStateCache(prevTabs[tabIndex]); const newTabs = prevTabs.filter(t => t.id !== tabId); setActiveTabId((prevActiveId) => { if (prevActiveId === tabId) { if (newTabs.length === 0) return null; const nextIndex = Math.min(tabIndex, newTabs.length - 1); return newTabs[nextIndex].id; } return prevActiveId; }); return newTabs; }); }, []); const closeOtherTabs = useCallback((tabId: string) => { setTabs((prevTabs) => { const keepTab = prevTabs.find(t => t.id === tabId); if (!keepTab) return prevTabs; prevTabs.filter(t => t.id !== tabId).forEach(clearTabStateCache); setActiveTabId(tabId); return [keepTab]; }); }, []); const closeAllTabs = useCallback(() => { setTabs((prevTabs) => { prevTabs.forEach(clearTabStateCache); return []; }); setActiveTabId(null); }, []); const closeTabsToTheLeft = useCallback((tabId: string) => { setTabs((prevTabs) => { const tabIndex = prevTabs.findIndex(t => t.id === tabId); if (tabIndex <= 0) return prevTabs; prevTabs.slice(0, tabIndex).forEach(clearTabStateCache); const newTabs = prevTabs.slice(tabIndex); setActiveTabId((prevActiveId) => { if (prevActiveId && !newTabs.find(t => t.id === prevActiveId)) { return tabId; } return prevActiveId; }); return newTabs; }); }, []); const closeTabsToTheRight = useCallback((tabId: string) => { setTabs((prevTabs) => { const tabIndex = prevTabs.findIndex(t => t.id === tabId); if (tabIndex === -1) return prevTabs; prevTabs.slice(tabIndex + 1).forEach(clearTabStateCache); const newTabs = prevTabs.slice(0, tabIndex + 1); setActiveTabId((prevActiveId) => { if (prevActiveId && !newTabs.find(t => t.id === prevActiveId)) { return tabId; } return prevActiveId; }); return newTabs; }); }, []); const switchTab = useCallback((tabId: string) => { setActiveTabId(tabId); }, []); const getActiveTab = useCallback(() => { return tabs.find(t => t.id === activeTabId); }, [tabs, activeTabId]); const refreshTab = useCallback((tabId: string) => { const tab = tabs.find(t => t.id === tabId); if (tab) clearTabStateCache(tab); setRefreshKeys(prev => ({ ...prev, [tabId]: (prev[tabId] || 0) + 1 })); }, [tabs]); const updateTabOrder = useCallback((newOrder: string[]) => { setTabs((prevTabs) => { const tabMap = new Map(prevTabs.map(t => [t.id, t])); const reordered = newOrder .map(id => tabMap.get(id)) .filter((t): t is TabItem => t !== undefined); // 드래그에 포함되지 않은 탭(오버플로우 등)은 뒤에 유지 const remaining = prevTabs.filter(t => !newOrder.includes(t.id)); return [...reordered, ...remaining]; }); }, []); return ( {children} ); } export function useTab() { const context = useContext(TabContext); if (context === undefined) { throw new Error("useTab must be used within a TabProvider"); } return context; }