"use client"; import React, { useState, useEffect, useMemo } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { X, Loader2 } from "lucide-react"; import type { TabsComponent, TabItem, TabInlineComponent, ComponentData } from "@/types/screen-management"; import { cn } from "@/lib/utils"; import { useActiveTab } from "@/contexts/ActiveTabContext"; import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; import { screenApi } from "@/lib/api/screen"; // 확장된 TabItem 타입 (screenId 지원) interface ExtendedTabItem extends TabItem { screenId?: number; screenName?: string; } interface TabsWidgetProps { component: TabsComponent; className?: string; style?: React.CSSProperties; menuObjid?: number; formData?: Record; onFormDataChange?: (fieldName: string, value: any) => void; isDesignMode?: boolean; onComponentSelect?: (tabId: string, componentId: string) => void; selectedComponentId?: string; // 테이블 선택된 행 데이터 (버튼 활성화 및 수정/삭제 동작에 필요) selectedRowsData?: any[]; onSelectedRowsChange?: ( selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[], ) => void; } export function TabsWidget({ component, className, style, menuObjid, formData = {}, onFormDataChange, isDesignMode = false, onComponentSelect, selectedComponentId, selectedRowsData, onSelectedRowsChange, }: 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(getInitialTab()); const [visibleTabs, setVisibleTabs] = useState(tabs as ExtendedTabItem[]); const [mountedTabs, setMountedTabs] = useState>(() => new Set([getInitialTab()])); // 🆕 화면 진입 시 첫 번째 탭 자동 선택 및 마운트 useEffect(() => { // 현재 선택된 탭이 유효하지 않거나 비어있으면 첫 번째 탭 선택 const validTabs = (tabs as ExtendedTabItem[]).filter((tab) => !tab.disabled); const firstValidTabId = validTabs[0]?.id; if (firstValidTabId) { // 선택된 탭이 없거나 유효하지 않으면 첫 번째 탭으로 설정 setSelectedTab((currentSelected) => { if (!currentSelected || !validTabs.some((t) => t.id === currentSelected)) { return firstValidTabId; } return currentSelected; }); // 첫 번째 탭이 mountedTabs에 없으면 추가 setMountedTabs((prev) => { const newSet = new Set(prev); // 첫 번째 탭 추가 if (firstValidTabId && !newSet.has(firstValidTabId)) { newSet.add(firstValidTabId); } return newSet; }); } }, [tabs]); // tabs가 변경될 때마다 실행 // screenId 기반 화면 로드 상태 const [screenLayouts, setScreenLayouts] = useState>({}); const [screenLoadingStates, setScreenLoadingStates] = useState>({}); const [screenErrors, setScreenErrors] = useState>({}); // 컴포넌트 탭 목록 변경 시 동기화 useEffect(() => { setVisibleTabs((tabs as ExtendedTabItem[]).filter((tab) => !tab.disabled)); }, [tabs]); // screenId가 있는 탭의 화면 레이아웃 로드 useEffect(() => { const loadScreenLayouts = async () => { for (const tab of visibleTabs) { const extTab = tab as ExtendedTabItem; // screenId가 있고, 아직 로드하지 않았으며, 인라인 컴포넌트가 없는 경우만 로드 if ( extTab.screenId && !screenLayouts[tab.id] && !screenLoadingStates[tab.id] && (!extTab.components || extTab.components.length === 0) ) { setScreenLoadingStates((prev) => ({ ...prev, [tab.id]: true })); try { const layoutData = await screenApi.getLayout(extTab.screenId); if (layoutData && layoutData.components) { setScreenLayouts((prev) => ({ ...prev, [tab.id]: layoutData.components })); } } catch (error) { console.error(`탭 "${tab.label}" 화면 로드 실패:`, error); setScreenErrors((prev) => ({ ...prev, [tab.id]: "화면을 불러올 수 없습니다." })); } finally { setScreenLoadingStates((prev) => ({ ...prev, [tab.id]: false })); } } } }; loadScreenLayouts(); }, [visibleTabs, screenLayouts, screenLoadingStates]); // 선택된 탭 변경 시 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}`; }; // 탭 컨텐츠 렌더링 (screenId 또는 인라인 컴포넌트) const renderTabContent = (tab: ExtendedTabItem) => { const extTab = tab as ExtendedTabItem; const inlineComponents = tab.components || []; // 1. screenId가 있고 인라인 컴포넌트가 없는 경우 -> 화면 로드 방식 if (extTab.screenId && inlineComponents.length === 0) { // 로딩 중 if (screenLoadingStates[tab.id]) { return (
화면을 불러오는 중...
); } // 에러 발생 if (screenErrors[tab.id]) { return (

{screenErrors[tab.id]}

); } // 화면 레이아웃이 로드된 경우 const loadedComponents = screenLayouts[tab.id]; if (loadedComponents && loadedComponents.length > 0) { return renderScreenComponents(loadedComponents); } // 아직 로드되지 않은 경우 return (
); } // 2. 인라인 컴포넌트가 있는 경우 -> 기존 v2 방식 if (inlineComponents.length > 0) { return renderInlineComponents(tab, inlineComponents); } // 3. 둘 다 없는 경우 return (

{isDesignMode ? "컴포넌트를 드래그하여 추가하세요" : "컴포넌트가 없습니다"}

); }; // screenId로 로드한 화면 컴포넌트 렌더링 const renderScreenComponents = (components: ComponentData[]) => { // InteractiveScreenViewerDynamic 동적 로드 const InteractiveScreenViewerDynamic = require("@/components/screen/InteractiveScreenViewerDynamic").InteractiveScreenViewerDynamic; // 컴포넌트들의 최대 위치를 계산하여 스크롤 가능한 영역 확보 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 (
{components.map((comp) => (
))}
); }; // 인라인 컴포넌트 렌더링 (v2 방식) const renderInlineComponents = (tab: TabItem, components: TabInlineComponent[]) => { // 컴포넌트들의 최대 위치를 계산하여 스크롤 가능한 영역 확보 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 (
{components.map((comp: TabInlineComponent) => { const isSelected = selectedComponentId === comp.id; return (
{ if (isDesignMode && onComponentSelect) { e.stopPropagation(); onComponentSelect(tab.id, comp.id); } }} >
); })}
); }; if (visibleTabs.length === 0) { return (

탭이 없습니다

); } return (
{visibleTabs.map((tab) => (
{tab.label} {tab.components && tab.components.length > 0 && ( ({tab.components.length}) )} {allowCloseable && ( )}
))}
{visibleTabs.map((tab) => { const shouldRender = mountedTabs.has(tab.id); const isActive = selectedTab === tab.id; return ( {shouldRender && renderTabContent(tab)} ); })}
); }