114 lines
3.4 KiB
TypeScript
114 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
import React, { Suspense, useRef, useState } from "react";
|
|
import { Loader2, Inbox } from "lucide-react";
|
|
import { useTab, TabItem } from "@/contexts/TabContext";
|
|
import { DialogPortalContainerContext } from "@/contexts/DialogPortalContext";
|
|
import { ScreenViewPageEmbeddable } from "@/app/(main)/screens/[screenId]/page";
|
|
|
|
function TabLoadingFallback() {
|
|
return (
|
|
<div className="flex h-full min-h-[400px] w-full items-center justify-center">
|
|
<div className="rounded-xl border border-slate-200 bg-white p-8 text-center shadow-lg">
|
|
<Loader2 className="text-primary mx-auto h-10 w-10 animate-spin" />
|
|
<p className="mt-4 font-medium text-slate-900">화면을 불러오는 중...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function EmptyTabState() {
|
|
return (
|
|
<div className="flex h-full w-full items-center justify-center bg-white">
|
|
<div className="flex flex-col items-center py-12 text-center">
|
|
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-slate-100">
|
|
<Inbox className="h-8 w-8 text-slate-400" />
|
|
</div>
|
|
<h3 className="mb-2 text-lg font-semibold text-slate-700">열린 탭이 없습니다</h3>
|
|
<p className="max-w-sm text-sm text-slate-500">
|
|
왼쪽 메뉴에서 화면을 선택하면 탭으로 열립니다.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 개별 탭 패널 - 탭별 Dialog 포탈 컨테이너를 제공
|
|
* 탭이 display:none이면 포탈 컨테이너도 숨겨져 모달이 자동으로 가려짐
|
|
*/
|
|
function TabPanel({
|
|
isActive,
|
|
isMounted,
|
|
children,
|
|
}: {
|
|
isActive: boolean;
|
|
isMounted: boolean;
|
|
children: React.ReactNode;
|
|
}) {
|
|
const [portalContainer, setPortalContainer] = useState<HTMLDivElement | null>(null);
|
|
|
|
return (
|
|
<div
|
|
className="h-full w-full"
|
|
style={{ display: isActive ? "block" : "none" }}
|
|
>
|
|
<div ref={setPortalContainer} />
|
|
<DialogPortalContainerContext.Provider value={portalContainer}>
|
|
{isMounted ? children : null}
|
|
</DialogPortalContainerContext.Provider>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function TabContent() {
|
|
const { tabs, activeTabId, refreshKeys } = useTab();
|
|
const stableOrderRef = useRef<TabItem[]>([]);
|
|
|
|
const mountedTabIdsRef = useRef<Set<string>>(
|
|
new Set(activeTabId ? [activeTabId] : [])
|
|
);
|
|
|
|
if (activeTabId && !mountedTabIdsRef.current.has(activeTabId)) {
|
|
mountedTabIdsRef.current.add(activeTabId);
|
|
}
|
|
|
|
const currentIds = new Set(tabs.map(t => t.id));
|
|
const stableIds = new Set(stableOrderRef.current.map(t => t.id));
|
|
|
|
stableOrderRef.current = stableOrderRef.current.filter(t => currentIds.has(t.id));
|
|
for (const id of mountedTabIdsRef.current) {
|
|
if (!currentIds.has(id)) mountedTabIdsRef.current.delete(id);
|
|
}
|
|
for (const tab of tabs) {
|
|
if (!stableIds.has(tab.id)) {
|
|
stableOrderRef.current.push(tab);
|
|
}
|
|
}
|
|
|
|
const stableTabs = stableOrderRef.current;
|
|
|
|
if (stableTabs.length === 0) {
|
|
return <EmptyTabState />;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{stableTabs.map((tab) => (
|
|
<TabPanel
|
|
key={`${tab.id}-${refreshKeys[tab.id] || 0}`}
|
|
isActive={tab.id === activeTabId}
|
|
isMounted={mountedTabIdsRef.current.has(tab.id)}
|
|
>
|
|
<Suspense fallback={<TabLoadingFallback />}>
|
|
<ScreenViewPageEmbeddable
|
|
screenId={tab.screenId}
|
|
menuObjid={tab.menuObjid}
|
|
/>
|
|
</Suspense>
|
|
</TabPanel>
|
|
))}
|
|
</>
|
|
);
|
|
}
|