탭 내부 컴포넌트 선택 및 업데이트 기능 추가: RealtimePreviewDynamic, ScreenDesigner, TabsWidget, DynamicComponentRenderer, TabsConfigPanel에서 탭 내부 컴포넌트를 선택하고 업데이트할 수 있는 콜백 함수를 추가하여 사용자 인터랙션을 개선하였습니다. 이를 통해 탭 내에서의 컴포넌트 관리가 용이해졌습니다.
This commit is contained in:
@@ -152,6 +152,11 @@ export interface DynamicComponentRendererProps {
|
||||
tableDisplayData?: any[]; // 🆕 화면 표시 데이터
|
||||
// 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용)
|
||||
flowSelectedData?: any[];
|
||||
// 🆕 컴포넌트 업데이트 콜백 (탭 내부 컴포넌트 위치 조정 등)
|
||||
onUpdateComponent?: (updatedComponent: any) => void;
|
||||
// 🆕 탭 내부 컴포넌트 선택 콜백
|
||||
onSelectTabComponent?: (tabId: string, compId: string, comp: any) => void;
|
||||
selectedTabComponentId?: string;
|
||||
flowSelectedStepId?: number | null;
|
||||
onFlowSelectedDataChange?: (selectedData: any[], stepId: number | null) => void;
|
||||
// 테이블 새로고침 키
|
||||
@@ -754,6 +759,11 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||
// 🆕 탭 관련 정보 전달 (탭 내부의 테이블 컴포넌트에서 사용)
|
||||
parentTabId: props.parentTabId,
|
||||
parentTabsComponentId: props.parentTabsComponentId,
|
||||
// 🆕 컴포넌트 업데이트 콜백 (탭 내부 컴포넌트 위치 조정 등)
|
||||
onUpdateComponent: props.onUpdateComponent,
|
||||
// 🆕 탭 내부 컴포넌트 선택 콜백
|
||||
onSelectTabComponent: props.onSelectTabComponent,
|
||||
selectedTabComponentId: props.selectedTabComponentId,
|
||||
};
|
||||
|
||||
// 렌더러가 클래스인지 함수인지 확인
|
||||
|
||||
@@ -1,21 +1,357 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import React, { useState, useRef, useCallback } from "react";
|
||||
import { ComponentRegistry } from "../../ComponentRegistry";
|
||||
import { ComponentCategory } from "@/types/component";
|
||||
import { Folder } from "lucide-react";
|
||||
import type { TabsComponent, TabItem } from "@/types/screen-management";
|
||||
import { Folder, Plus, Move, Settings, Trash2 } from "lucide-react";
|
||||
import type { TabItem, TabInlineComponent } from "@/types/screen-management";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
||||
|
||||
// 디자인 모드용 탭 에디터 컴포넌트
|
||||
const TabsDesignEditor: React.FC<{
|
||||
component: any;
|
||||
tabs: TabItem[];
|
||||
onUpdateComponent?: (updatedComponent: any) => void;
|
||||
onSelectTabComponent?: (tabId: string, compId: string, comp: TabInlineComponent) => void;
|
||||
selectedTabComponentId?: string;
|
||||
}> = ({ component, tabs, onUpdateComponent, onSelectTabComponent, selectedTabComponentId }) => {
|
||||
const [activeTabId, setActiveTabId] = useState<string>(tabs[0]?.id || "");
|
||||
const [draggingCompId, setDraggingCompId] = useState<string | null>(null);
|
||||
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const activeTab = tabs.find((t) => t.id === activeTabId);
|
||||
|
||||
const getTabStyle = (tab: TabItem) => {
|
||||
const isActive = tab.id === activeTabId;
|
||||
return cn(
|
||||
"px-4 py-2 text-sm font-medium cursor-pointer transition-colors",
|
||||
isActive
|
||||
? "bg-background border-b-2 border-primary text-primary"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
||||
);
|
||||
};
|
||||
|
||||
// 컴포넌트 삭제
|
||||
const handleDeleteComponent = useCallback(
|
||||
(compId: string) => {
|
||||
if (!onUpdateComponent) return;
|
||||
|
||||
const updatedTabs = tabs.map((tab) => {
|
||||
if (tab.id === activeTabId) {
|
||||
return {
|
||||
...tab,
|
||||
components: (tab.components || []).filter((c) => c.id !== compId),
|
||||
};
|
||||
}
|
||||
return tab;
|
||||
});
|
||||
|
||||
onUpdateComponent({
|
||||
...component,
|
||||
componentConfig: {
|
||||
...component.componentConfig,
|
||||
tabs: updatedTabs,
|
||||
},
|
||||
});
|
||||
},
|
||||
[activeTabId, component, onUpdateComponent, tabs]
|
||||
);
|
||||
|
||||
// 컴포넌트 드래그 시작
|
||||
const handleDragStart = useCallback(
|
||||
(e: React.MouseEvent, comp: TabInlineComponent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const targetElement = (e.currentTarget as HTMLElement);
|
||||
const targetRect = targetElement.getBoundingClientRect();
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
|
||||
// 스크롤 위치 고려
|
||||
const scrollLeft = containerRef.current.scrollLeft;
|
||||
const scrollTop = containerRef.current.scrollTop;
|
||||
|
||||
// 마우스 클릭 위치에서 컴포넌트의 좌상단까지의 오프셋
|
||||
const offsetX = e.clientX - targetRect.left;
|
||||
const offsetY = e.clientY - targetRect.top;
|
||||
|
||||
// 초기 컨테이너 위치 저장
|
||||
const initialContainerX = containerRect.left;
|
||||
const initialContainerY = containerRect.top;
|
||||
|
||||
setDraggingCompId(comp.id);
|
||||
setDragOffset({ x: offsetX, y: offsetY });
|
||||
|
||||
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
// 현재 컨테이너의 위치 가져오기 (스크롤/리사이즈 고려)
|
||||
const currentContainerRect = containerRef.current.getBoundingClientRect();
|
||||
const currentScrollLeft = containerRef.current.scrollLeft;
|
||||
const currentScrollTop = containerRef.current.scrollTop;
|
||||
|
||||
// 컨테이너 내에서의 위치 계산 (스크롤 포함)
|
||||
const newX = moveEvent.clientX - currentContainerRect.left - offsetX + currentScrollLeft;
|
||||
const newY = moveEvent.clientY - currentContainerRect.top - offsetY + currentScrollTop;
|
||||
|
||||
// 실시간 위치 업데이트 (시각적 피드백)
|
||||
const draggedElement = document.querySelector(
|
||||
`[data-tab-comp-id="${comp.id}"]`
|
||||
) as HTMLElement;
|
||||
if (draggedElement) {
|
||||
draggedElement.style.left = `${Math.max(0, newX)}px`;
|
||||
draggedElement.style.top = `${Math.max(0, newY)}px`;
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = (upEvent: MouseEvent) => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
setDraggingCompId(null);
|
||||
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentContainerRect = containerRef.current.getBoundingClientRect();
|
||||
const currentScrollLeft = containerRef.current.scrollLeft;
|
||||
const currentScrollTop = containerRef.current.scrollTop;
|
||||
|
||||
const newX = upEvent.clientX - currentContainerRect.left - offsetX + currentScrollLeft;
|
||||
const newY = upEvent.clientY - currentContainerRect.top - offsetY + currentScrollTop;
|
||||
|
||||
// 탭 컴포넌트 위치 업데이트
|
||||
if (onUpdateComponent) {
|
||||
const updatedTabs = tabs.map((tab) => {
|
||||
if (tab.id === activeTabId) {
|
||||
return {
|
||||
...tab,
|
||||
components: (tab.components || []).map((c) =>
|
||||
c.id === comp.id
|
||||
? {
|
||||
...c,
|
||||
position: {
|
||||
x: Math.max(0, Math.round(newX)),
|
||||
y: Math.max(0, Math.round(newY)),
|
||||
},
|
||||
}
|
||||
: c
|
||||
),
|
||||
};
|
||||
}
|
||||
return tab;
|
||||
});
|
||||
|
||||
onUpdateComponent({
|
||||
...component,
|
||||
componentConfig: {
|
||||
...component.componentConfig,
|
||||
tabs: updatedTabs,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
setDraggingCompId(null);
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
},
|
||||
[activeTabId, component, onUpdateComponent, tabs]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col overflow-hidden rounded-lg border bg-background">
|
||||
{/* 탭 헤더 */}
|
||||
<div className="flex items-center border-b bg-muted/30">
|
||||
{tabs.length > 0 ? (
|
||||
tabs.map((tab) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className={getTabStyle(tab)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setActiveTabId(tab.id);
|
||||
onSelectTabComponent?.(null);
|
||||
}}
|
||||
>
|
||||
{tab.label || "탭"}
|
||||
{tab.components && tab.components.length > 0 && (
|
||||
<span className="ml-1 text-xs text-muted-foreground">
|
||||
({tab.components.length})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="px-4 py-2 text-sm text-muted-foreground">
|
||||
탭이 없습니다
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 탭 컨텐츠 영역 - 드롭 영역 */}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative flex-1 overflow-hidden"
|
||||
data-tabs-container="true"
|
||||
data-component-id={component.id}
|
||||
data-active-tab-id={activeTabId}
|
||||
onClick={() => onSelectTabComponent?.(activeTabId, "", {} as TabInlineComponent)}
|
||||
>
|
||||
{activeTab ? (
|
||||
<div className="absolute inset-0 overflow-auto p-2">
|
||||
{activeTab.components && activeTab.components.length > 0 ? (
|
||||
<div className="relative h-full w-full">
|
||||
{activeTab.components.map((comp: TabInlineComponent) => {
|
||||
const isSelected = selectedTabComponentId === comp.id;
|
||||
const isDragging = draggingCompId === comp.id;
|
||||
|
||||
// 컴포넌트 데이터를 DynamicComponentRenderer 형식으로 변환
|
||||
const componentData = {
|
||||
id: comp.id,
|
||||
type: "component" as const,
|
||||
componentType: comp.componentType,
|
||||
label: comp.label,
|
||||
position: comp.position || { x: 0, y: 0 },
|
||||
size: comp.size || { width: 200, height: 100 },
|
||||
componentConfig: comp.componentConfig || {},
|
||||
style: comp.style || {},
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={comp.id}
|
||||
data-tab-comp-id={comp.id}
|
||||
className={cn(
|
||||
"absolute rounded border bg-white shadow-sm transition-all",
|
||||
isSelected
|
||||
? "border-primary ring-2 ring-primary/30"
|
||||
: "border-gray-200 hover:border-primary/50",
|
||||
isDragging && "opacity-80 shadow-lg"
|
||||
)}
|
||||
style={{
|
||||
left: comp.position?.x || 0,
|
||||
top: comp.position?.y || 0,
|
||||
width: comp.size?.width || 200,
|
||||
height: comp.size?.height || 100,
|
||||
zIndex: isDragging ? 100 : isSelected ? 10 : 1,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectTabComponent?.(activeTabId, comp.id, comp);
|
||||
}}
|
||||
>
|
||||
{/* 드래그 핸들 - 상단 */}
|
||||
<div
|
||||
className="absolute left-0 right-0 top-0 z-10 flex h-5 cursor-move items-center justify-between bg-gray-100/80 px-1"
|
||||
onMouseDown={(e) => handleDragStart(e, comp)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<Move className="h-3 w-3 text-gray-400" />
|
||||
<span className="text-[10px] text-gray-500 truncate max-w-[120px]">
|
||||
{comp.label || comp.componentType}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
className="rounded p-0.5 hover:bg-gray-200"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectTabComponent?.(activeTabId, comp.id, comp);
|
||||
}}
|
||||
title="설정"
|
||||
>
|
||||
<Settings className="h-3 w-3 text-gray-500" />
|
||||
</button>
|
||||
<button
|
||||
className="rounded p-0.5 hover:bg-red-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteComponent(comp.id);
|
||||
}}
|
||||
title="삭제"
|
||||
>
|
||||
<Trash2 className="h-3 w-3 text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 실제 컴포넌트 렌더링 */}
|
||||
<div className="h-full w-full pt-5 overflow-hidden pointer-events-none">
|
||||
<DynamicComponentRenderer
|
||||
component={componentData as any}
|
||||
isDesignMode={true}
|
||||
formData={{}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50/50">
|
||||
<Plus className="mb-2 h-8 w-8 text-gray-400" />
|
||||
<p className="text-sm font-medium text-gray-500">
|
||||
컴포넌트를 드래그하여 추가
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-gray-400">
|
||||
좌측 패널에서 컴포넌트를 이 영역에 드롭하세요
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
설정 패널에서 탭을 추가하세요
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// TabsWidget 래퍼 컴포넌트
|
||||
const TabsWidgetWrapper: React.FC<any> = (props) => {
|
||||
const { component, ...restProps } = props;
|
||||
|
||||
const {
|
||||
component,
|
||||
isDesignMode,
|
||||
onUpdateComponent,
|
||||
onSelectTabComponent,
|
||||
selectedTabComponentId,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
// componentConfig에서 탭 정보 추출
|
||||
const tabsConfig = component.componentConfig || {};
|
||||
const tabs: TabItem[] = tabsConfig.tabs || [];
|
||||
|
||||
// 🎯 디자인 모드에서는 드롭 가능한 에디터 UI 렌더링
|
||||
if (isDesignMode) {
|
||||
return (
|
||||
<TabsDesignEditor
|
||||
component={component}
|
||||
tabs={tabs}
|
||||
onUpdateComponent={onUpdateComponent}
|
||||
onSelectTabComponent={onSelectTabComponent}
|
||||
selectedTabComponentId={selectedTabComponentId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 실행 모드에서는 TabsWidget 렌더링
|
||||
const tabsComponent = {
|
||||
...component,
|
||||
type: "tabs" as const,
|
||||
tabs: tabsConfig.tabs || [],
|
||||
tabs: tabs,
|
||||
defaultTab: tabsConfig.defaultTab,
|
||||
orientation: tabsConfig.orientation || "horizontal",
|
||||
variant: tabsConfig.variant || "default",
|
||||
@@ -23,10 +359,9 @@ const TabsWidgetWrapper: React.FC<any> = (props) => {
|
||||
persistSelection: tabsConfig.persistSelection || false,
|
||||
};
|
||||
|
||||
const TabsWidget =
|
||||
require("@/components/screen/widgets/TabsWidget").TabsWidget;
|
||||
|
||||
// TabsWidget 동적 로드
|
||||
const TabsWidget = require("@/components/screen/widgets/TabsWidget").TabsWidget;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<TabsWidget component={tabsComponent} {...restProps} />
|
||||
@@ -36,26 +371,49 @@ const TabsWidgetWrapper: React.FC<any> = (props) => {
|
||||
|
||||
/**
|
||||
* 탭 컴포넌트 정의
|
||||
*
|
||||
* 여러 화면을 탭으로 구분하여 전환할 수 있는 컴포넌트
|
||||
*
|
||||
* 탭별로 컴포넌트를 자유롭게 배치할 수 있는 레이아웃 컴포넌트
|
||||
*/
|
||||
ComponentRegistry.registerComponent({
|
||||
id: "v2-tabs-widget",
|
||||
name: "탭 컴포넌트",
|
||||
description: "화면을 탭으로 전환할 수 있는 컴포넌트입니다. 각 탭마다 다른 화면을 연결할 수 있습니다.",
|
||||
description:
|
||||
"탭별로 컴포넌트를 자유롭게 배치할 수 있는 레이아웃 컴포넌트입니다.",
|
||||
category: ComponentCategory.LAYOUT,
|
||||
webType: "text" as any, // 레이아웃 컴포넌트이므로 임시값
|
||||
component: TabsWidgetWrapper, // ✅ 실제 TabsWidget 렌더러
|
||||
defaultConfig: {},
|
||||
tags: ["tabs", "navigation", "layout", "screen"],
|
||||
webType: "text" as any,
|
||||
component: TabsWidgetWrapper,
|
||||
defaultConfig: {
|
||||
tabs: [
|
||||
{
|
||||
id: "tab-1",
|
||||
label: "탭 1",
|
||||
order: 0,
|
||||
disabled: false,
|
||||
components: [],
|
||||
},
|
||||
{
|
||||
id: "tab-2",
|
||||
label: "탭 2",
|
||||
order: 1,
|
||||
disabled: false,
|
||||
components: [],
|
||||
},
|
||||
],
|
||||
defaultTab: "tab-1",
|
||||
orientation: "horizontal",
|
||||
variant: "default",
|
||||
allowCloseable: false,
|
||||
persistSelection: false,
|
||||
},
|
||||
tags: ["tabs", "navigation", "layout", "container"],
|
||||
icon: Folder,
|
||||
version: "1.0.0",
|
||||
|
||||
version: "2.0.0",
|
||||
|
||||
defaultSize: {
|
||||
width: 800,
|
||||
height: 600,
|
||||
},
|
||||
|
||||
|
||||
defaultProps: {
|
||||
type: "tabs" as const,
|
||||
tabs: [
|
||||
@@ -64,12 +422,14 @@ ComponentRegistry.registerComponent({
|
||||
label: "탭 1",
|
||||
order: 0,
|
||||
disabled: false,
|
||||
components: [],
|
||||
},
|
||||
{
|
||||
id: "tab-2",
|
||||
label: "탭 2",
|
||||
order: 1,
|
||||
disabled: false,
|
||||
components: [],
|
||||
},
|
||||
] as TabItem[],
|
||||
defaultTab: "tab-1",
|
||||
@@ -78,82 +438,167 @@ ComponentRegistry.registerComponent({
|
||||
allowCloseable: false,
|
||||
persistSelection: false,
|
||||
},
|
||||
|
||||
// 에디터 모드에서의 렌더링
|
||||
renderEditor: ({ component, isSelected, onClick, onDragStart, onDragEnd, children }) => {
|
||||
const tabsComponent = component as TabsComponent;
|
||||
const tabs = tabsComponent.tabs || [];
|
||||
|
||||
|
||||
// 에디터 모드에서의 렌더링 - 탭 선택 및 컴포넌트 드롭 지원
|
||||
renderEditor: ({
|
||||
component,
|
||||
isSelected,
|
||||
onClick,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
}) => {
|
||||
const tabsConfig = (component as any).componentConfig || {};
|
||||
const tabs: TabItem[] = tabsConfig.tabs || [];
|
||||
|
||||
// 에디터 모드에서 선택된 탭 상태 관리
|
||||
const [activeTabId, setActiveTabId] = useState<string>(
|
||||
tabs[0]?.id || ""
|
||||
);
|
||||
|
||||
const activeTab = tabs.find((t) => t.id === activeTabId);
|
||||
|
||||
// 탭 스타일 클래스
|
||||
const getTabStyle = (tab: TabItem) => {
|
||||
const isActive = tab.id === activeTabId;
|
||||
return cn(
|
||||
"px-4 py-2 text-sm font-medium cursor-pointer transition-colors",
|
||||
isActive
|
||||
? "bg-background border-b-2 border-primary text-primary"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50"
|
||||
className="flex h-full w-full flex-col overflow-hidden rounded-lg border bg-background"
|
||||
onClick={onClick}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<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: TabItem, index: number) => (
|
||||
<span
|
||||
key={tab.id}
|
||||
className="rounded-md border bg-white px-2 py-1 text-xs"
|
||||
>
|
||||
{tab.label || `탭 ${index + 1}`}
|
||||
</span>
|
||||
))}
|
||||
{/* 탭 헤더 */}
|
||||
<div className="flex items-center border-b bg-muted/30">
|
||||
{tabs.length > 0 ? (
|
||||
tabs.map((tab) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className={getTabStyle(tab)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setActiveTabId(tab.id);
|
||||
}}
|
||||
>
|
||||
{tab.label || "탭"}
|
||||
{tab.components && tab.components.length > 0 && (
|
||||
<span className="ml-1 text-xs text-muted-foreground">
|
||||
({tab.components.length})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="px-4 py-2 text-sm text-muted-foreground">
|
||||
탭이 없습니다
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 탭 컨텐츠 영역 - 드롭 영역 */}
|
||||
<div
|
||||
className="relative flex-1 overflow-hidden"
|
||||
data-tabs-container="true"
|
||||
data-component-id={component.id}
|
||||
data-active-tab-id={activeTabId}
|
||||
>
|
||||
{activeTab ? (
|
||||
<div className="absolute inset-0 overflow-auto p-2">
|
||||
{activeTab.components && activeTab.components.length > 0 ? (
|
||||
<div className="relative h-full w-full">
|
||||
{activeTab.components.map((comp: TabInlineComponent) => (
|
||||
<div
|
||||
key={comp.id}
|
||||
className="absolute rounded border border-dashed border-gray-300 bg-white/80 p-2 shadow-sm"
|
||||
style={{
|
||||
left: comp.position?.x || 0,
|
||||
top: comp.position?.y || 0,
|
||||
width: comp.size?.width || 200,
|
||||
height: comp.size?.height || 100,
|
||||
}}
|
||||
>
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
<span className="text-xs font-medium text-gray-600">
|
||||
{comp.label || comp.componentType}
|
||||
</span>
|
||||
<span className="text-[10px] text-gray-400">
|
||||
{comp.componentType}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50/50">
|
||||
<Plus className="mb-2 h-8 w-8 text-gray-400" />
|
||||
<p className="text-sm font-medium text-gray-500">
|
||||
컴포넌트를 드래그하여 추가
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-gray-400">
|
||||
좌측 패널에서 컴포넌트를 이 영역에 드롭하세요
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
설정 패널에서 탭을 추가하세요
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 선택 표시 */}
|
||||
{isSelected && (
|
||||
<div className="pointer-events-none absolute inset-0 rounded-lg ring-2 ring-primary ring-offset-2" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
// 인터랙티브 모드에서의 렌더링 (실제 동작)
|
||||
|
||||
// 인터랙티브 모드에서의 렌더링
|
||||
renderInteractive: ({ component }) => {
|
||||
// InteractiveScreenViewer에서 TabsWidget을 사용하므로 여기서는 null 반환
|
||||
return null;
|
||||
},
|
||||
|
||||
// 설정 패널 (동적 로딩)
|
||||
configPanel: React.lazy(() =>
|
||||
import("@/components/screen/config-panels/TabsConfigPanel").then(module => ({
|
||||
default: module.TabsConfigPanel
|
||||
}))
|
||||
|
||||
// 설정 패널
|
||||
configPanel: React.lazy(() =>
|
||||
import("@/components/screen/config-panels/TabsConfigPanel").then(
|
||||
(module) => ({
|
||||
default: module.TabsConfigPanel,
|
||||
})
|
||||
)
|
||||
),
|
||||
|
||||
|
||||
// 검증 함수
|
||||
validate: (component) => {
|
||||
const tabsComponent = component as TabsComponent;
|
||||
const tabsConfig = (component as any).componentConfig || {};
|
||||
const tabs: TabItem[] = tabsConfig.tabs || [];
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!tabsComponent.tabs || tabsComponent.tabs.length === 0) {
|
||||
|
||||
if (!tabs || tabs.length === 0) {
|
||||
errors.push("최소 1개 이상의 탭이 필요합니다.");
|
||||
}
|
||||
|
||||
if (tabsComponent.tabs) {
|
||||
const tabIds = tabsComponent.tabs.map((t) => t.id);
|
||||
|
||||
if (tabs) {
|
||||
const tabIds = tabs.map((t) => t.id);
|
||||
const uniqueIds = new Set(tabIds);
|
||||
if (tabIds.length !== uniqueIds.size) {
|
||||
errors.push("탭 ID가 중복되었습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// console.log("✅ 탭 컴포넌트 등록 완료");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user