탭 내부 컴포넌트 선택 및 업데이트 기능 추가: RealtimePreviewDynamic, ScreenDesigner, TabsWidget, DynamicComponentRenderer, TabsConfigPanel에서 탭 내부 컴포넌트를 선택하고 업데이트할 수 있는 콜백 함수를 추가하여 사용자 인터랙션을 개선하였습니다. 이를 통해 탭 내에서의 컴포넌트 관리가 용이해졌습니다.
This commit is contained in:
@@ -1,23 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import React, { useState, useEffect } 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 } from "@/types/screen-management";
|
||||
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
|
||||
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; // 부모 화면의 메뉴 OBJID
|
||||
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 }: TabsWidgetProps) {
|
||||
// ActiveTab context 사용
|
||||
export function TabsWidget({
|
||||
component,
|
||||
className,
|
||||
style,
|
||||
menuObjid,
|
||||
formData = {},
|
||||
onFormDataChange,
|
||||
isDesignMode = false,
|
||||
onComponentSelect,
|
||||
selectedComponentId,
|
||||
}: TabsWidgetProps) {
|
||||
const { setActiveTab, removeTabsComponent } = useActiveTab();
|
||||
const {
|
||||
tabs = [],
|
||||
@@ -28,7 +42,6 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge
|
||||
persistSelection = false,
|
||||
} = component;
|
||||
|
||||
|
||||
const storageKey = `tabs-${component.id}-selected`;
|
||||
|
||||
// 초기 선택 탭 결정
|
||||
@@ -44,9 +57,6 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState<string>(getInitialTab());
|
||||
const [visibleTabs, setVisibleTabs] = useState<TabItem[]>(tabs);
|
||||
const [loadingScreens, setLoadingScreens] = useState<Record<string, boolean>>({});
|
||||
const [screenLayouts, setScreenLayouts] = useState<Record<number, any>>({});
|
||||
// 🆕 한 번이라도 선택된 탭 추적 (지연 로딩 + 캐싱)
|
||||
const [mountedTabs, setMountedTabs] = useState<Set<string>>(() => new Set([getInitialTab()]));
|
||||
|
||||
// 컴포넌트 탭 목록 변경 시 동기화
|
||||
@@ -59,14 +69,12 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge
|
||||
if (persistSelection && typeof window !== "undefined") {
|
||||
localStorage.setItem(storageKey, selectedTab);
|
||||
}
|
||||
|
||||
// ActiveTab Context에 현재 활성 탭 정보 등록
|
||||
const currentTabInfo = visibleTabs.find(t => t.id === selectedTab);
|
||||
|
||||
const currentTabInfo = visibleTabs.find((t) => t.id === selectedTab);
|
||||
if (currentTabInfo) {
|
||||
setActiveTab(component.id, {
|
||||
tabId: selectedTab,
|
||||
tabsComponentId: component.id,
|
||||
screenId: currentTabInfo.screenId,
|
||||
label: currentTabInfo.label,
|
||||
});
|
||||
}
|
||||
@@ -79,53 +87,16 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge
|
||||
};
|
||||
}, [component.id, removeTabsComponent]);
|
||||
|
||||
// 초기 로드 시 선택된 탭의 화면 불러오기
|
||||
useEffect(() => {
|
||||
const currentTab = visibleTabs.find((t) => t.id === selectedTab);
|
||||
if (currentTab && currentTab.screenId && !screenLayouts[currentTab.screenId]) {
|
||||
loadScreenLayout(currentTab.screenId);
|
||||
}
|
||||
}, [selectedTab, visibleTabs]);
|
||||
|
||||
// 화면 레이아웃 로드
|
||||
const loadScreenLayout = async (screenId: number) => {
|
||||
if (screenLayouts[screenId]) {
|
||||
return; // 이미 로드됨
|
||||
}
|
||||
|
||||
setLoadingScreens((prev) => ({ ...prev, [screenId]: true }));
|
||||
|
||||
try {
|
||||
const { apiClient } = await import("@/lib/api/client");
|
||||
const response = await apiClient.get(`/screen-management/screens/${screenId}/layout`);
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
setScreenLayouts((prev) => ({ ...prev, [screenId]: response.data.data }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`화면 레이아웃 로드 실패 ${screenId}:`, error);
|
||||
} finally {
|
||||
setLoadingScreens((prev) => ({ ...prev, [screenId]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
// 탭 변경 핸들러
|
||||
const handleTabChange = (tabId: string) => {
|
||||
setSelectedTab(tabId);
|
||||
|
||||
// 마운트된 탭 목록에 추가 (한 번 마운트되면 유지)
|
||||
setMountedTabs(prev => {
|
||||
|
||||
setMountedTabs((prev) => {
|
||||
if (prev.has(tabId)) return prev;
|
||||
const newSet = new Set(prev);
|
||||
newSet.add(tabId);
|
||||
return newSet;
|
||||
});
|
||||
|
||||
// 해당 탭의 화면 로드
|
||||
const tab = visibleTabs.find((t) => t.id === tabId);
|
||||
if (tab && tab.screenId && !screenLayouts[tab.screenId]) {
|
||||
loadScreenLayout(tab.screenId);
|
||||
}
|
||||
};
|
||||
|
||||
// 탭 닫기 핸들러
|
||||
@@ -135,7 +106,6 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge
|
||||
const updatedTabs = visibleTabs.filter((tab) => tab.id !== tabId);
|
||||
setVisibleTabs(updatedTabs);
|
||||
|
||||
// 닫은 탭이 선택된 탭이었다면 다음 탭 선택
|
||||
if (selectedTab === tabId && updatedTabs.length > 0) {
|
||||
setSelectedTab(updatedTabs[0].id);
|
||||
}
|
||||
@@ -153,6 +123,68 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
{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">
|
||||
@@ -162,7 +194,7 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col pt-4" style={style}>
|
||||
<div className={cn("flex h-full w-full flex-col pt-4", className)} style={style}>
|
||||
<Tabs
|
||||
value={selectedTab}
|
||||
onValueChange={handleTabChange}
|
||||
@@ -175,6 +207,11 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge
|
||||
<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
|
||||
@@ -191,86 +228,19 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
{/* 🆕 forceMount + CSS 숨김으로 탭 전환 시 리렌더링 방지 */}
|
||||
<div className="relative flex-1 overflow-hidden">
|
||||
{visibleTabs.map((tab) => {
|
||||
// 한 번도 선택되지 않은 탭은 렌더링하지 않음 (지연 로딩)
|
||||
const shouldRender = mountedTabs.has(tab.id);
|
||||
const isActive = selectedTab === tab.id;
|
||||
|
||||
|
||||
return (
|
||||
<TabsContent
|
||||
key={tab.id}
|
||||
value={tab.id}
|
||||
forceMount // 🆕 DOM에 항상 유지
|
||||
className={cn(
|
||||
"h-full",
|
||||
!isActive && "hidden" // 🆕 비활성 탭은 CSS로 숨김
|
||||
)}
|
||||
<TabsContent
|
||||
key={tab.id}
|
||||
value={tab.id}
|
||||
forceMount
|
||||
className={cn("h-full", !isActive && "hidden")}
|
||||
>
|
||||
{/* 한 번 마운트된 탭만 내용 렌더링 */}
|
||||
{shouldRender && (
|
||||
<>
|
||||
{tab.screenId ? (
|
||||
loadingScreens[tab.screenId] ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
<span className="text-muted-foreground ml-2">화면 로딩 중...</span>
|
||||
</div>
|
||||
) : screenLayouts[tab.screenId] ? (
|
||||
(() => {
|
||||
const layoutData = screenLayouts[tab.screenId];
|
||||
const { components = [], screenResolution } = layoutData;
|
||||
|
||||
|
||||
const designWidth = screenResolution?.width || 1920;
|
||||
const designHeight = screenResolution?.height || 1080;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative h-full w-full overflow-auto bg-background"
|
||||
style={{
|
||||
minHeight: `${designHeight}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="relative"
|
||||
style={{
|
||||
width: `${designWidth}px`,
|
||||
height: `${designHeight}px`,
|
||||
margin: "0 auto",
|
||||
}}
|
||||
>
|
||||
{components.map((comp: any) => (
|
||||
<InteractiveScreenViewerDynamic
|
||||
key={comp.id}
|
||||
component={comp}
|
||||
allComponents={components}
|
||||
screenInfo={{
|
||||
id: tab.screenId,
|
||||
tableName: layoutData.tableName,
|
||||
}}
|
||||
menuObjid={menuObjid}
|
||||
parentTabId={tab.id}
|
||||
parentTabsComponentId={component.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<p className="text-muted-foreground text-sm">화면을 불러올 수 없습니다</p>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{shouldRender && renderTabComponents(tab)}
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user