249 lines
8.4 KiB
TypeScript
249 lines
8.4 KiB
TypeScript
"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,
|
|
domFormFields: formFields ?? undefined,
|
|
});
|
|
}
|
|
|
|
// 새 활성 탭의 스크롤 + 폼 상태 복원
|
|
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, {
|
|
scrollPositions: finalPositions,
|
|
domFormFields: formFields ?? undefined,
|
|
});
|
|
};
|
|
|
|
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;
|
|
}
|