ERP-node/frontend/components/screen/widgets/TabsWidget.tsx

268 lines
8.4 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";
import type { TabsComponent, TabItem, TabInlineComponent } from "@/types/screen-management";
import { cn } from "@/lib/utils";
import { useActiveTab } from "@/contexts/ActiveTabContext";
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
interface TabsWidgetProps {
component: TabsComponent;
className?: string;
style?: React.CSSProperties;
menuObjid?: number;
formData?: Record<string, any>;
onFormDataChange?: (data: Record<string, any>) => void;
isDesignMode?: boolean; // 디자인 모드 여부
onComponentSelect?: (tabId: string, componentId: string) => void; // 컴포넌트 선택 콜백
selectedComponentId?: string; // 선택된 컴포넌트 ID
}
export function TabsWidget({
component,
className,
style,
menuObjid,
formData = {},
onFormDataChange,
isDesignMode = false,
onComponentSelect,
selectedComponentId,
}: TabsWidgetProps) {
const { setActiveTab, removeTabsComponent } = useActiveTab();
const {
tabs = [],
defaultTab,
orientation = "horizontal",
variant = "default",
allowCloseable = false,
persistSelection = false,
} = component;
const storageKey = `tabs-${component.id}-selected`;
// 초기 선택 탭 결정
const getInitialTab = () => {
if (persistSelection && typeof window !== "undefined") {
const saved = localStorage.getItem(storageKey);
if (saved && tabs.some((t) => t.id === saved)) {
return saved;
}
}
return defaultTab || tabs[0]?.id || "";
};
const [selectedTab, setSelectedTab] = useState<string>(getInitialTab());
const [visibleTabs, setVisibleTabs] = useState<TabItem[]>(tabs);
const [mountedTabs, setMountedTabs] = useState<Set<string>>(() => new Set([getInitialTab()]));
// 컴포넌트 탭 목록 변경 시 동기화
useEffect(() => {
setVisibleTabs(tabs.filter((tab) => !tab.disabled));
}, [tabs]);
// 선택된 탭 변경 시 localStorage에 저장 + ActiveTab Context 업데이트
useEffect(() => {
if (persistSelection && typeof window !== "undefined") {
localStorage.setItem(storageKey, selectedTab);
}
const currentTabInfo = visibleTabs.find((t) => t.id === selectedTab);
if (currentTabInfo) {
setActiveTab(component.id, {
tabId: selectedTab,
tabsComponentId: component.id,
label: currentTabInfo.label,
});
}
}, [selectedTab, persistSelection, storageKey, component.id, visibleTabs, setActiveTab]);
// 컴포넌트 언마운트 시 ActiveTab Context에서 제거
useEffect(() => {
return () => {
removeTabsComponent(component.id);
};
}, [component.id, removeTabsComponent]);
// 탭 변경 핸들러
const handleTabChange = (tabId: string) => {
setSelectedTab(tabId);
setMountedTabs((prev) => {
if (prev.has(tabId)) return prev;
const newSet = new Set(prev);
newSet.add(tabId);
return newSet;
});
};
// 탭 닫기 핸들러
const handleCloseTab = (tabId: string, e: React.MouseEvent) => {
e.stopPropagation();
const updatedTabs = visibleTabs.filter((tab) => tab.id !== tabId);
setVisibleTabs(updatedTabs);
if (selectedTab === tabId && updatedTabs.length > 0) {
setSelectedTab(updatedTabs[0].id);
}
};
// 탭 스타일 클래스
const getTabsListClass = () => {
const baseClass = orientation === "vertical" ? "flex-col" : "";
const variantClass =
variant === "pills"
? "bg-muted p-1 rounded-lg"
: variant === "underline"
? "border-b"
: "bg-muted p-1";
return `${baseClass} ${variantClass}`;
};
// 인라인 컴포넌트 렌더링
const renderTabComponents = (tab: TabItem) => {
const components = tab.components || [];
if (components.length === 0) {
return (
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50">
<p className="text-muted-foreground text-sm">
{isDesignMode ? "컴포넌트를 드래그하여 추가하세요" : "컴포넌트가 없습니다"}
</p>
</div>
);
}
// 컴포넌트들의 최대 위치를 계산하여 스크롤 가능한 영역 확보
const maxBottom = Math.max(
...components.map((c) => (c.position?.y || 0) + (c.size?.height || 100)),
300 // 최소 높이
);
const maxRight = Math.max(
...components.map((c) => (c.position?.x || 0) + (c.size?.width || 200)),
400 // 최소 너비
);
return (
<div
className="relative"
style={{
minHeight: maxBottom + 20,
minWidth: maxRight + 20,
}}
>
{components.map((comp: TabInlineComponent) => {
const isSelected = selectedComponentId === comp.id;
return (
<div
key={comp.id}
className={cn(
"absolute",
isDesignMode && "cursor-move",
isDesignMode && isSelected && "ring-2 ring-primary ring-offset-2"
)}
style={{
left: comp.position?.x || 0,
top: comp.position?.y || 0,
width: comp.size?.width || 200,
height: comp.size?.height || 100,
}}
onClick={(e) => {
if (isDesignMode && onComponentSelect) {
e.stopPropagation();
onComponentSelect(tab.id, comp.id);
}
}}
>
<DynamicComponentRenderer
component={{
id: comp.id,
componentType: comp.componentType,
label: comp.label,
position: comp.position,
size: comp.size,
componentConfig: comp.componentConfig || {},
style: comp.style,
}}
formData={formData}
onFormDataChange={onFormDataChange}
menuObjid={menuObjid}
isDesignMode={isDesignMode}
/>
</div>
);
})}
</div>
);
};
if (visibleTabs.length === 0) {
return (
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50">
<p className="text-muted-foreground text-sm"> </p>
</div>
);
}
return (
<div className={cn("flex h-full w-full flex-col pt-4", className)} style={style}>
<Tabs
value={selectedTab}
onValueChange={handleTabChange}
orientation={orientation}
className="flex h-full w-full flex-col"
>
<div className="relative z-10">
<TabsList className={getTabsListClass()}>
{visibleTabs.map((tab) => (
<div key={tab.id} className="relative">
<TabsTrigger value={tab.id} disabled={tab.disabled} className="relative pr-8">
{tab.label}
{tab.components && tab.components.length > 0 && (
<span className="ml-1 text-xs text-muted-foreground">
({tab.components.length})
</span>
)}
</TabsTrigger>
{allowCloseable && (
<Button
onClick={(e) => handleCloseTab(tab.id, e)}
variant="ghost"
size="sm"
className="absolute right-1 top-1/2 h-5 w-5 -translate-y-1/2 p-0 hover:bg-destructive/10"
>
<X className="h-3 w-3" />
</Button>
)}
</div>
))}
</TabsList>
</div>
<div className="relative flex-1 overflow-auto">
{visibleTabs.map((tab) => {
const shouldRender = mountedTabs.has(tab.id);
const isActive = selectedTab === tab.id;
return (
<TabsContent
key={tab.id}
value={tab.id}
forceMount
className={cn("h-full overflow-auto", !isActive && "hidden")}
>
{shouldRender && renderTabComponents(tab)}
</TabsContent>
);
})}
</div>
</Tabs>
</div>
);
}