탭 컴포넌트 구현

This commit is contained in:
kjs 2025-11-25 10:06:56 +09:00
parent a46a2a664f
commit 5e2392c417
4 changed files with 238 additions and 94 deletions

View File

@ -346,6 +346,14 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
// 실제 사용 가능한 위젯 렌더링
const renderInteractiveWidget = (comp: ComponentData) => {
console.log("🎯 renderInteractiveWidget 호출:", {
type: comp.type,
id: comp.id,
componentId: (comp as any).componentId,
hasComponentConfig: !!(comp as any).componentConfig,
componentConfig: (comp as any).componentConfig,
});
// 데이터 테이블 컴포넌트 처리
if (comp.type === "datatable") {
return (
@ -398,7 +406,8 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
}
// 탭 컴포넌트 처리
if (comp.type === "tabs" || (comp.type === "component" && comp.componentId === "tabs-widget")) {
const componentType = (comp as any).componentType || (comp as any).componentId;
if (comp.type === "tabs" || (comp.type === "component" && componentType === "tabs-widget")) {
const TabsWidget = require("@/components/screen/widgets/TabsWidget").TabsWidget;
// componentConfig에서 탭 정보 추출
@ -416,6 +425,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
console.log("🔍 탭 컴포넌트 렌더링:", {
originalType: comp.type,
componentType,
componentId: (comp as any).componentId,
tabs: tabsComponent.tabs,
tabsConfig,

View File

@ -555,41 +555,70 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
})()}
{/* 탭 컴포넌트 타입 */}
{(type === "tabs" || (type === "component" && (component as any).componentId === "tabs-widget")) &&
{(type === "tabs" || (type === "component" && ((component as any).componentType === "tabs-widget" || (component as any).componentId === "tabs-widget"))) &&
(() => {
const tabsComponent = component as any;
// componentConfig에서 탭 정보 가져오기
const tabs = tabsComponent.componentConfig?.tabs || tabsComponent.tabs || [];
console.log("🎯 탭 컴포넌트 조건 충족:", {
type,
componentType: (component as any).componentType,
componentId: (component as any).componentId,
isDesignMode,
});
return (
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50">
<div className="text-center">
<div className="flex items-center justify-center">
<Folder className="h-8 w-8 text-gray-400" />
</div>
<p className="text-muted-foreground mt-2 text-sm font-medium"> </p>
<p className="text-xs text-gray-400">
{tabs.length > 0
? `${tabs.length}개의 탭 (실제 화면에서 표시됩니다)`
: "탭이 없습니다. 설정 패널에서 탭을 추가하세요"}
</p>
{tabs.length > 0 && (
<div className="mt-2 flex flex-wrap justify-center gap-1">
{tabs.map((tab: any, index: number) => (
<Badge key={tab.id} variant="outline" className="text-xs">
{tab.label || `${index + 1}`}
{tab.screenName && (
<span className="ml-1 text-[10px] text-gray-400">
({tab.screenName})
</span>
)}
</Badge>
))}
if (isDesignMode) {
// 디자인 모드: 미리보기 표시
const tabsComponent = component as any;
const tabs = tabsComponent.componentConfig?.tabs || tabsComponent.tabs || [];
return (
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50">
<div className="text-center">
<div className="flex items-center justify-center">
<Folder className="h-8 w-8 text-gray-400" />
</div>
)}
<p className="text-muted-foreground mt-2 text-sm font-medium"> </p>
<p className="text-xs text-gray-400">
{tabs.length > 0
? `${tabs.length}개의 탭 (실제 화면에서 표시됩니다)`
: "탭이 없습니다. 설정 패널에서 탭을 추가하세요"}
</p>
{tabs.length > 0 && (
<div className="mt-2 flex flex-wrap justify-center gap-1">
{tabs.map((tab: any, index: number) => (
<Badge key={tab.id} variant="outline" className="text-xs">
{tab.label || `${index + 1}`}
{tab.screenName && (
<span className="ml-1 text-[10px] text-gray-400">
({tab.screenName})
</span>
)}
</Badge>
))}
</div>
)}
</div>
</div>
</div>
);
);
} else {
// 실제 화면: TabsWidget 렌더링
const TabsWidget = require("@/components/screen/widgets/TabsWidget").TabsWidget;
const tabsConfig = (component as any).componentConfig || {};
const tabsComponent = {
...component,
type: "tabs" as const,
tabs: tabsConfig.tabs || [],
defaultTab: tabsConfig.defaultTab,
orientation: tabsConfig.orientation || "horizontal",
variant: tabsConfig.variant || "default",
allowCloseable: tabsConfig.allowCloseable || false,
persistSelection: tabsConfig.persistSelection || false,
};
return (
<div className="h-full w-full">
<TabsWidget component={tabsComponent as any} />
</div>
);
}
})()}
{/* 그룹 타입 */}

View File

@ -60,24 +60,45 @@ export function TabsWidget({ component, className, style }: TabsWidgetProps) {
}
}, [selectedTab, persistSelection, storageKey]);
// 초기 로드 시 선택된 탭의 화면 불러오기
useEffect(() => {
const currentTab = visibleTabs.find((t) => t.id === selectedTab);
console.log("🔄 초기 탭 로드:", {
selectedTab,
currentTab,
hasScreenId: !!currentTab?.screenId,
screenId: currentTab?.screenId,
});
if (currentTab && currentTab.screenId && !screenLayouts[currentTab.screenId]) {
console.log("📥 초기 화면 로딩 시작:", currentTab.screenId);
loadScreenLayout(currentTab.screenId);
}
}, [selectedTab, visibleTabs]);
// 화면 레이아웃 로드
const loadScreenLayout = async (screenId: number) => {
if (screenLayouts[screenId]) {
console.log("✅ 이미 로드된 화면:", screenId);
return; // 이미 로드됨
}
console.log("📥 화면 레이아웃 로딩 시작:", screenId);
setLoadingScreens((prev) => ({ ...prev, [screenId]: true }));
try {
const response = await fetch(`/api/screen-management/screens/${screenId}/layout`);
if (response.ok) {
const data = await response.json();
if (data.success && data.data) {
setScreenLayouts((prev) => ({ ...prev, [screenId]: data.data }));
}
const { apiClient } = await import("@/lib/api/client");
const response = await apiClient.get(`/screen-management/screens/${screenId}/layout`);
console.log("📦 API 응답:", { screenId, success: response.data.success, hasData: !!response.data.data });
if (response.data.success && response.data.data) {
console.log("✅ 화면 레이아웃 로드 완료:", screenId);
setScreenLayouts((prev) => ({ ...prev, [screenId]: response.data.data }));
} else {
console.error("❌ 화면 레이아웃 로드 실패 - success false");
}
} catch (error) {
console.error(`Failed to load screen layout ${screenId}:`, error);
console.error(`❌ 화면 레이아웃 로드 실패 ${screenId}:`, error);
} finally {
setLoadingScreens((prev) => ({ ...prev, [screenId]: false }));
}
@ -85,11 +106,15 @@ export function TabsWidget({ component, className, style }: TabsWidgetProps) {
// 탭 변경 핸들러
const handleTabChange = (tabId: string) => {
console.log("🔄 탭 변경:", tabId);
setSelectedTab(tabId);
// 해당 탭의 화면 로드
const tab = visibleTabs.find((t) => t.id === tabId);
console.log("🔍 선택된 탭 정보:", { tab, hasScreenId: !!tab?.screenId, screenId: tab?.screenId });
if (tab && tab.screenId && !screenLayouts[tab.screenId]) {
console.log("📥 탭 변경 시 화면 로딩:", tab.screenId);
loadScreenLayout(tab.screenId);
}
};
@ -120,6 +145,7 @@ export function TabsWidget({ component, className, style }: TabsWidgetProps) {
};
if (visibleTabs.length === 0) {
console.log("⚠️ 보이는 탭이 없음");
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>
@ -127,61 +153,106 @@ export function TabsWidget({ component, className, style }: TabsWidgetProps) {
);
}
return (
<Tabs
value={selectedTab}
onValueChange={handleTabChange}
orientation={orientation}
className={className}
style={style}
>
<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}
</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>
console.log("🎨 TabsWidget 최종 렌더링:", {
visibleTabsCount: visibleTabs.length,
selectedTab,
screenLayoutsKeys: Object.keys(screenLayouts),
loadingScreensKeys: Object.keys(loadingScreens),
});
{visibleTabs.map((tab) => (
<TabsContent key={tab.id} value={tab.id} className="h-full w-full">
{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>
return (
<div className="flex h-full w-full flex-col pt-4" 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}
</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>
) : screenLayouts[tab.screenId] ? (
<div className="h-full w-full overflow-auto">
<InteractiveScreenViewerDynamic
component={screenLayouts[tab.screenId].components[0]}
allComponents={screenLayouts[tab.screenId].components}
/>
</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>
)}
</TabsContent>
))}
</Tabs>
))}
</TabsList>
</div>
<div className="relative flex-1 overflow-hidden">
{visibleTabs.map((tab) => (
<TabsContent key={tab.id} value={tab.id} className="h-full">
{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;
console.log("🎯 렌더링할 화면 데이터:", {
screenId: tab.screenId,
componentsCount: components.length,
screenResolution,
});
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((component: any) => (
<InteractiveScreenViewerDynamic
key={component.id}
component={component}
allComponents={components}
/>
))}
</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>
)}
</TabsContent>
))}
</div>
</Tabs>
</div>
);
}

View File

@ -6,6 +6,40 @@ import { ComponentCategory } from "@/types/component";
import { Folder } from "lucide-react";
import type { TabsComponent, TabItem } from "@/types/screen-management";
// TabsWidget 래퍼 컴포넌트
const TabsWidgetWrapper: React.FC<any> = (props) => {
const { component, ...restProps } = props;
// componentConfig에서 탭 정보 추출
const tabsConfig = component.componentConfig || {};
const tabsComponent = {
...component,
type: "tabs" as const,
tabs: tabsConfig.tabs || [],
defaultTab: tabsConfig.defaultTab,
orientation: tabsConfig.orientation || "horizontal",
variant: tabsConfig.variant || "default",
allowCloseable: tabsConfig.allowCloseable || false,
persistSelection: tabsConfig.persistSelection || false,
};
console.log("🎨 TabsWidget 렌더링:", {
componentId: component.id,
tabs: tabsComponent.tabs,
tabsLength: tabsComponent.tabs.length,
component,
});
// TabsWidget 동적 로드
const TabsWidget = require("@/components/screen/widgets/TabsWidget").TabsWidget;
return (
<div className="h-full w-full">
<TabsWidget component={tabsComponent} {...restProps} />
</div>
);
};
/**
*
*
@ -17,7 +51,7 @@ ComponentRegistry.registerComponent({
description: "화면을 탭으로 전환할 수 있는 컴포넌트입니다. 각 탭마다 다른 화면을 연결할 수 있습니다.",
category: ComponentCategory.LAYOUT,
webType: "text" as any, // 레이아웃 컴포넌트이므로 임시값
component: () => null as any, // 레이아웃 컴포넌트이므로 임시값
component: TabsWidgetWrapper, // ✅ 실제 TabsWidget 렌더러
defaultConfig: {},
tags: ["tabs", "navigation", "layout", "screen"],
icon: Folder,