ERP-node/frontend/components/layout/TabContent.tsx

248 lines
8.3 KiB
TypeScript
Raw Normal View History

"use client";
import React, { useRef, useEffect, useCallback } from "react";
import { useTabStore, selectTabs, selectActiveTabId } from "@/stores/tabStore";
import { ScreenViewPageWrapper } from "@/app/(main)/screens/[screenId]/page";
import { AdminPageRenderer } from "./AdminPageRenderer";
import { EmptyDashboard } from "./EmptyDashboard";
import { TabIdProvider } from "@/contexts/TabIdContext";
import { registerModalPortal } from "@/lib/modalPortalRef";
import ScreenModal from "@/components/common/ScreenModal";
import {
saveTabCacheImmediate,
loadTabCache,
captureAllScrollPositions,
restoreAllScrollPositions,
getElementPath,
captureFormState,
restoreFormState,
clearTabCache,
} from "@/lib/tabStateCache";
export function TabContent() {
const tabs = useTabStore(selectTabs);
const activeTabId = useTabStore(selectActiveTabId);
const refreshKeys = useTabStore((s) => s.refreshKeys);
// 한 번이라도 활성화된 탭만 마운트 (지연 마운트)
const mountedTabIdsRef = useRef<Set<string>>(new Set());
// 각 탭의 스크롤 컨테이너 ref
const scrollRefsMap = useRef<Map<string, HTMLDivElement | null>>(new Map());
// 이전 활성 탭 ID 추적
const prevActiveTabIdRef = useRef<string | null>(null);
// 활성 탭의 스크롤 위치를 실시간 추적 (display:none 전에 캡처하기 위함)
// Map<tabId, Map<elementPath, {top, left}>> - 탭 내 여러 스크롤 영역을 각각 추적
const lastScrollMapRef = useRef<Map<string, Map<string, { top: number; left: number }>>>(new Map());
// 요소 → 경로 캐시 (매 스크롤 이벤트마다 경로를 재계산하지 않기 위함)
const pathCacheRef = useRef<WeakMap<HTMLElement, string | null>>(new WeakMap());
if (activeTabId) {
mountedTabIdsRef.current.add(activeTabId);
}
// 활성 탭의 scroll 이벤트를 감지하여 요소별 위치를 실시간 저장
useEffect(() => {
if (!activeTabId) return;
const container = scrollRefsMap.current.get(activeTabId);
if (!container) return;
const handleScroll = (e: Event) => {
const target = e.target as HTMLElement;
let path = pathCacheRef.current.get(target);
if (path === undefined) {
path = getElementPath(target, container);
pathCacheRef.current.set(target, path);
}
if (path === null) return;
let tabMap = lastScrollMapRef.current.get(activeTabId);
if (!tabMap) {
tabMap = new Map();
lastScrollMapRef.current.set(activeTabId, tabMap);
}
if (target.scrollTop > 0 || target.scrollLeft > 0) {
tabMap.set(path, { top: target.scrollTop, left: target.scrollLeft });
} else {
tabMap.delete(path);
}
};
container.addEventListener("scroll", handleScroll, true);
return () => container.removeEventListener("scroll", handleScroll, true);
}, [activeTabId]);
// 복원 관련 cleanup ref
const scrollRestoreCleanupRef = useRef<(() => void) | null>(null);
const formRestoreCleanupRef = useRef<(() => void) | null>(null);
// 탭 전환 시: 이전 탭 상태 캐싱, 새 탭 상태 복원
useEffect(() => {
// 이전 복원 작업 취소
if (scrollRestoreCleanupRef.current) {
scrollRestoreCleanupRef.current();
scrollRestoreCleanupRef.current = null;
}
if (formRestoreCleanupRef.current) {
formRestoreCleanupRef.current();
formRestoreCleanupRef.current = null;
}
const prevId = prevActiveTabIdRef.current;
// 이전 활성 탭의 스크롤 + 폼 상태 저장
if (prevId && prevId !== activeTabId) {
const tabMap = lastScrollMapRef.current.get(prevId);
const scrollPositions =
tabMap && tabMap.size > 0
? Array.from(tabMap.entries()).map(([path, pos]) => ({ path, ...pos }))
: undefined;
const prevEl = scrollRefsMap.current.get(prevId);
const formFields = captureFormState(prevEl ?? null);
saveTabCacheImmediate(prevId, {
...(scrollPositions && { scrollPositions }),
...(formFields && { domFormFields: formFields }),
});
}
// 새 활성 탭의 스크롤 + 폼 상태 복원
if (activeTabId) {
const cache = loadTabCache(activeTabId);
if (cache) {
const el = scrollRefsMap.current.get(activeTabId);
if (cache.scrollPositions) {
const cleanup = restoreAllScrollPositions(el ?? null, cache.scrollPositions);
if (cleanup) scrollRestoreCleanupRef.current = cleanup;
}
if (cache.domFormFields) {
const cleanup = restoreFormState(el ?? null, cache.domFormFields ?? null);
if (cleanup) formRestoreCleanupRef.current = cleanup;
}
}
}
prevActiveTabIdRef.current = activeTabId;
}, [activeTabId]);
// F5 새로고침 직전에 활성 탭의 스크롤/폼 상태를 저장
useEffect(() => {
const handleBeforeUnload = () => {
const currentActiveId = prevActiveTabIdRef.current;
if (!currentActiveId) return;
const el = scrollRefsMap.current.get(currentActiveId);
// 활성 탭은 display:block이므로 DOM에서 직접 캡처 (가장 정확)
const scrollPositions = captureAllScrollPositions(el ?? null);
// DOM 캡처 실패 시 실시간 추적 데이터 fallback
const tabMap = lastScrollMapRef.current.get(currentActiveId);
const trackedPositions =
!scrollPositions && tabMap && tabMap.size > 0
? Array.from(tabMap.entries()).map(([path, pos]) => ({ path, ...pos }))
: undefined;
const finalPositions = scrollPositions || trackedPositions;
const formFields = captureFormState(el ?? null);
saveTabCacheImmediate(currentActiveId, {
...(finalPositions && { scrollPositions: finalPositions }),
...(formFields && { domFormFields: formFields }),
});
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
if (scrollRestoreCleanupRef.current) scrollRestoreCleanupRef.current();
if (formRestoreCleanupRef.current) formRestoreCleanupRef.current();
};
}, []);
// 탭 닫기 시 캐시 정리 (tabs 배열 변화 감지)
useEffect(() => {
const currentTabIds = new Set(tabs.map((t) => t.id));
const mountedIds = mountedTabIdsRef.current;
mountedIds.forEach((id) => {
if (!currentTabIds.has(id)) {
clearTabCache(id);
scrollRefsMap.current.delete(id);
mountedIds.delete(id);
}
});
}, [tabs]);
const setScrollRef = useCallback((tabId: string, el: HTMLDivElement | null) => {
scrollRefsMap.current.set(tabId, el);
}, []);
// 포탈 컨테이너 ref callback: 전역 레퍼런스에 등록
const portalRefCallback = useCallback((el: HTMLDivElement | null) => {
registerModalPortal(el);
}, []);
if (tabs.length === 0) {
return <EmptyDashboard />;
}
const tabLookup = new Map(tabs.map((t) => [t.id, t]));
const stableIds = Array.from(mountedTabIdsRef.current);
return (
<div ref={portalRefCallback} className="relative min-h-0 flex-1 overflow-hidden">
{stableIds.map((tabId) => {
const tab = tabLookup.get(tabId);
if (!tab) return null;
const isActive = tab.id === activeTabId;
const refreshKey = refreshKeys[tab.id] || 0;
return (
<div
key={tab.id}
ref={(el) => setScrollRef(tab.id, el)}
className="absolute inset-0 overflow-hidden"
style={{ display: isActive ? "block" : "none" }}
>
<TabIdProvider value={tab.id}>
<TabPageRenderer tab={tab} refreshKey={refreshKey} />
<ScreenModal key={`modal-${tab.id}-${refreshKey}`} />
</TabIdProvider>
</div>
);
})}
</div>
);
}
function TabPageRenderer({
tab,
refreshKey,
}: {
tab: { id: string; type: string; screenId?: number; menuObjid?: number; adminUrl?: string };
refreshKey: number;
}) {
if (tab.type === "screen" && tab.screenId != null) {
return (
<ScreenViewPageWrapper
key={`${tab.id}-${refreshKey}`}
screenIdProp={tab.screenId}
menuObjidProp={tab.menuObjid}
/>
);
}
if (tab.type === "admin" && tab.adminUrl) {
return (
<div key={`${tab.id}-${refreshKey}`} className="h-full">
<AdminPageRenderer url={tab.adminUrl} />
</div>
);
}
return null;
}