225 lines
6.9 KiB
TypeScript
225 lines
6.9 KiB
TypeScript
"use client";
|
|
|
|
import { create } from "zustand";
|
|
import { devtools, persist } from "zustand/middleware";
|
|
import { clearTabCache } from "@/lib/tabStateCache";
|
|
|
|
// --- 타입 정의 ---
|
|
|
|
export type AppMode = "user" | "admin";
|
|
|
|
export interface Tab {
|
|
id: string;
|
|
type: "screen" | "admin";
|
|
title: string;
|
|
screenId?: number;
|
|
menuObjid?: number;
|
|
adminUrl?: string;
|
|
}
|
|
|
|
interface ModeTabData {
|
|
tabs: Tab[];
|
|
activeTabId: string | null;
|
|
}
|
|
|
|
interface TabState {
|
|
mode: AppMode;
|
|
user: ModeTabData;
|
|
admin: ModeTabData;
|
|
refreshKeys: Record<string, number>;
|
|
|
|
setMode: (mode: AppMode) => void;
|
|
|
|
openTab: (tab: Omit<Tab, "id">, insertIndex?: number) => void;
|
|
closeTab: (tabId: string) => void;
|
|
switchTab: (tabId: string) => void;
|
|
refreshTab: (tabId: string) => void;
|
|
|
|
closeOtherTabs: (tabId: string) => void;
|
|
closeTabsToLeft: (tabId: string) => void;
|
|
closeTabsToRight: (tabId: string) => void;
|
|
closeAllTabs: () => void;
|
|
|
|
updateTabOrder: (fromIndex: number, toIndex: number) => void;
|
|
}
|
|
|
|
// --- 헬퍼 함수 ---
|
|
|
|
function generateTabId(tab: Omit<Tab, "id">): string {
|
|
if (tab.type === "screen" && tab.screenId != null) {
|
|
return `tab-screen-${tab.screenId}-${tab.menuObjid ?? 0}`;
|
|
}
|
|
if (tab.type === "admin" && tab.adminUrl) {
|
|
return `tab-admin-${tab.adminUrl.replace(/[^a-zA-Z0-9]/g, "-")}`;
|
|
}
|
|
return `tab-${Date.now()}`;
|
|
}
|
|
|
|
function findDuplicateTab(tabs: Tab[], newTab: Omit<Tab, "id">): Tab | undefined {
|
|
if (newTab.type === "screen" && newTab.screenId != null) {
|
|
return tabs.find(
|
|
(t) => t.type === "screen" && t.screenId === newTab.screenId && t.menuObjid === newTab.menuObjid,
|
|
);
|
|
}
|
|
if (newTab.type === "admin" && newTab.adminUrl) {
|
|
return tabs.find((t) => t.type === "admin" && t.adminUrl === newTab.adminUrl);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function getNextActiveTabId(tabs: Tab[], closedTabId: string, currentActiveId: string | null): string | null {
|
|
if (currentActiveId !== closedTabId) return currentActiveId;
|
|
const idx = tabs.findIndex((t) => t.id === closedTabId);
|
|
if (idx === -1) return null;
|
|
const remaining = tabs.filter((t) => t.id !== closedTabId);
|
|
if (remaining.length === 0) return null;
|
|
if (idx > 0) return remaining[Math.min(idx - 1, remaining.length - 1)].id;
|
|
return remaining[0].id;
|
|
}
|
|
|
|
// 현재 모드의 데이터 키 반환
|
|
function modeKey(state: TabState): AppMode {
|
|
return state.mode;
|
|
}
|
|
|
|
// --- 셀렉터 (컴포넌트에서 사용) ---
|
|
|
|
export function selectTabs(state: TabState): Tab[] {
|
|
return state[state.mode].tabs;
|
|
}
|
|
|
|
export function selectActiveTabId(state: TabState): string | null {
|
|
return state[state.mode].activeTabId;
|
|
}
|
|
|
|
// --- Store ---
|
|
|
|
const EMPTY_MODE: ModeTabData = { tabs: [], activeTabId: null };
|
|
|
|
export const useTabStore = create<TabState>()(
|
|
devtools(
|
|
persist(
|
|
(set, get) => ({
|
|
mode: "user" as AppMode,
|
|
user: { ...EMPTY_MODE },
|
|
admin: { ...EMPTY_MODE },
|
|
refreshKeys: {},
|
|
|
|
setMode: (mode) => {
|
|
set({ mode });
|
|
},
|
|
|
|
openTab: (tabData, insertIndex) => {
|
|
const mk = modeKey(get());
|
|
const modeData = get()[mk];
|
|
const existing = findDuplicateTab(modeData.tabs, tabData);
|
|
|
|
if (existing) {
|
|
set({ [mk]: { ...modeData, activeTabId: existing.id } });
|
|
return;
|
|
}
|
|
|
|
const id = generateTabId(tabData);
|
|
const newTab: Tab = { ...tabData, id };
|
|
const newTabs = [...modeData.tabs];
|
|
|
|
if (insertIndex != null && insertIndex >= 0 && insertIndex <= newTabs.length) {
|
|
newTabs.splice(insertIndex, 0, newTab);
|
|
} else {
|
|
newTabs.push(newTab);
|
|
}
|
|
|
|
set({ [mk]: { tabs: newTabs, activeTabId: id } });
|
|
},
|
|
|
|
closeTab: (tabId) => {
|
|
clearTabCache(tabId);
|
|
const mk = modeKey(get());
|
|
const modeData = get()[mk];
|
|
const nextActive = getNextActiveTabId(modeData.tabs, tabId, modeData.activeTabId);
|
|
const newTabs = modeData.tabs.filter((t) => t.id !== tabId);
|
|
const { [tabId]: _, ...restKeys } = get().refreshKeys;
|
|
set({ [mk]: { tabs: newTabs, activeTabId: nextActive }, refreshKeys: restKeys });
|
|
},
|
|
|
|
switchTab: (tabId) => {
|
|
const mk = modeKey(get());
|
|
const modeData = get()[mk];
|
|
set({ [mk]: { ...modeData, activeTabId: tabId } });
|
|
},
|
|
|
|
refreshTab: (tabId) => {
|
|
set((state) => ({
|
|
refreshKeys: { ...state.refreshKeys, [tabId]: (state.refreshKeys[tabId] || 0) + 1 },
|
|
}));
|
|
},
|
|
|
|
closeOtherTabs: (tabId) => {
|
|
const mk = modeKey(get());
|
|
const modeData = get()[mk];
|
|
modeData.tabs.filter((t) => t.id !== tabId).forEach((t) => clearTabCache(t.id));
|
|
set({ [mk]: { tabs: modeData.tabs.filter((t) => t.id === tabId), activeTabId: tabId } });
|
|
},
|
|
|
|
closeTabsToLeft: (tabId) => {
|
|
const mk = modeKey(get());
|
|
const modeData = get()[mk];
|
|
const idx = modeData.tabs.findIndex((t) => t.id === tabId);
|
|
if (idx === -1) return;
|
|
modeData.tabs.slice(0, idx).forEach((t) => clearTabCache(t.id));
|
|
set({ [mk]: { tabs: modeData.tabs.slice(idx), activeTabId: tabId } });
|
|
},
|
|
|
|
closeTabsToRight: (tabId) => {
|
|
const mk = modeKey(get());
|
|
const modeData = get()[mk];
|
|
const idx = modeData.tabs.findIndex((t) => t.id === tabId);
|
|
if (idx === -1) return;
|
|
modeData.tabs.slice(idx + 1).forEach((t) => clearTabCache(t.id));
|
|
set({ [mk]: { tabs: modeData.tabs.slice(0, idx + 1), activeTabId: tabId } });
|
|
},
|
|
|
|
closeAllTabs: () => {
|
|
const mk = modeKey(get());
|
|
const modeData = get()[mk];
|
|
modeData.tabs.forEach((t) => clearTabCache(t.id));
|
|
set({ [mk]: { tabs: [], activeTabId: null } });
|
|
},
|
|
|
|
updateTabOrder: (fromIndex, toIndex) => {
|
|
const mk = modeKey(get());
|
|
const modeData = get()[mk];
|
|
const newTabs = [...modeData.tabs];
|
|
const [moved] = newTabs.splice(fromIndex, 1);
|
|
newTabs.splice(toIndex, 0, moved);
|
|
set({ [mk]: { ...modeData, tabs: newTabs } });
|
|
},
|
|
}),
|
|
{
|
|
name: "erp-tab-store",
|
|
storage: {
|
|
getItem: (name) => {
|
|
if (typeof window === "undefined") return null;
|
|
const raw = sessionStorage.getItem(name);
|
|
return raw ? JSON.parse(raw) : null;
|
|
},
|
|
setItem: (name, value) => {
|
|
if (typeof window === "undefined") return;
|
|
sessionStorage.setItem(name, JSON.stringify(value));
|
|
},
|
|
removeItem: (name) => {
|
|
if (typeof window === "undefined") return;
|
|
sessionStorage.removeItem(name);
|
|
},
|
|
},
|
|
partialize: (state) => ({
|
|
mode: state.mode,
|
|
user: state.user,
|
|
admin: state.admin,
|
|
}) as unknown as TabState,
|
|
},
|
|
),
|
|
{ name: "TabStore" },
|
|
),
|
|
);
|