- Modified the `getTableSchema` function in `adminController.ts` to use company-specific column labels when available, falling back to common labels if not. - Adjusted the SQL query to join `table_type_columns` for both company-specific and common labels, ensuring the correct display order is maintained. - Removed unnecessary component count display in the `TabsDesignEditor` to streamline the UI. These changes enhance the accuracy of the table schema representation based on company context and improve the overall user interface by simplifying tab displays.
756 lines
27 KiB
TypeScript
756 lines
27 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useRef, useCallback, useEffect } from "react";
|
|
import { ComponentRegistry } from "../../ComponentRegistry";
|
|
import { ComponentCategory } from "@/types/component";
|
|
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 [dragPosition, setDragPosition] = useState<{ x: number; y: number } | null>(null);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const rafRef = useRef<number | null>(null);
|
|
|
|
// 리사이즈 상태
|
|
const [resizingCompId, setResizingCompId] = useState<string | null>(null);
|
|
const [resizeSize, setResizeSize] = useState<{ width: number; height: number } | null>(null);
|
|
const [lastResizedCompId, setLastResizedCompId] = useState<string | null>(null);
|
|
|
|
const activeTab = tabs.find((t) => t.id === activeTabId);
|
|
|
|
// 🆕 탭 컴포넌트 size가 업데이트되면 resizeSize 초기화
|
|
useEffect(() => {
|
|
if (resizeSize && lastResizedCompId && !resizingCompId) {
|
|
const targetComp = activeTab?.components?.find(c => c.id === lastResizedCompId);
|
|
if (targetComp &&
|
|
targetComp.size?.width === resizeSize.width &&
|
|
targetComp.size?.height === resizeSize.height) {
|
|
setResizeSize(null);
|
|
setLastResizedCompId(null);
|
|
}
|
|
}
|
|
}, [tabs, activeTab, resizeSize, lastResizedCompId, resizingCompId]);
|
|
|
|
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]
|
|
);
|
|
|
|
// 10px 단위 스냅 함수
|
|
const snapTo10 = useCallback((value: number) => Math.round(value / 10) * 10, []);
|
|
|
|
// 컴포넌트 드래그 시작
|
|
const handleDragStart = useCallback(
|
|
(e: React.MouseEvent, comp: TabInlineComponent) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
|
|
// 드래그 시작 시 마우스 위치와 컴포넌트의 현재 위치 저장
|
|
const startMouseX = e.clientX;
|
|
const startMouseY = e.clientY;
|
|
const startLeft = comp.position?.x || 0;
|
|
const startTop = comp.position?.y || 0;
|
|
|
|
setDraggingCompId(comp.id);
|
|
setDragPosition({ x: startLeft, y: startTop });
|
|
|
|
const handleMouseMove = (moveEvent: MouseEvent) => {
|
|
// requestAnimationFrame으로 성능 최적화
|
|
if (rafRef.current) {
|
|
cancelAnimationFrame(rafRef.current);
|
|
}
|
|
|
|
rafRef.current = requestAnimationFrame(() => {
|
|
// 마우스 이동량 계산
|
|
const deltaX = moveEvent.clientX - startMouseX;
|
|
const deltaY = moveEvent.clientY - startMouseY;
|
|
|
|
// 새 위치 = 시작 위치 + 이동량 (10px 단위 스냅 적용)
|
|
const newX = snapTo10(Math.max(0, startLeft + deltaX));
|
|
const newY = snapTo10(Math.max(0, startTop + deltaY));
|
|
|
|
// React 상태로 위치 업데이트 (리렌더링 트리거)
|
|
setDragPosition({ x: newX, y: newY });
|
|
});
|
|
};
|
|
|
|
const handleMouseUp = (upEvent: MouseEvent) => {
|
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
|
|
if (rafRef.current) {
|
|
cancelAnimationFrame(rafRef.current);
|
|
rafRef.current = null;
|
|
}
|
|
|
|
// 마우스 이동량 계산
|
|
const deltaX = upEvent.clientX - startMouseX;
|
|
const deltaY = upEvent.clientY - startMouseY;
|
|
|
|
// 새 위치 = 시작 위치 + 이동량 (10px 단위 스냅 적용)
|
|
const newX = snapTo10(Math.max(0, startLeft + deltaX));
|
|
const newY = snapTo10(Math.max(0, startTop + deltaY));
|
|
|
|
setDraggingCompId(null);
|
|
setDragPosition(null);
|
|
|
|
// 탭 컴포넌트 위치 업데이트
|
|
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: newX,
|
|
y: 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, snapTo10]
|
|
);
|
|
|
|
// 리사이즈 시작 핸들러
|
|
const handleResizeStart = useCallback(
|
|
(e: React.MouseEvent, comp: TabInlineComponent, direction: "e" | "s" | "se") => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
|
|
const startMouseX = e.clientX;
|
|
const startMouseY = e.clientY;
|
|
const startWidth = comp.size?.width || 200;
|
|
const startHeight = comp.size?.height || 100;
|
|
|
|
setResizingCompId(comp.id);
|
|
setResizeSize({ width: startWidth, height: startHeight });
|
|
|
|
const handleMouseMove = (moveEvent: MouseEvent) => {
|
|
if (rafRef.current) {
|
|
cancelAnimationFrame(rafRef.current);
|
|
}
|
|
|
|
rafRef.current = requestAnimationFrame(() => {
|
|
const deltaX = moveEvent.clientX - startMouseX;
|
|
const deltaY = moveEvent.clientY - startMouseY;
|
|
|
|
let newWidth = startWidth;
|
|
let newHeight = startHeight;
|
|
|
|
if (direction === "e" || direction === "se") {
|
|
newWidth = snapTo10(Math.max(50, startWidth + deltaX));
|
|
}
|
|
if (direction === "s" || direction === "se") {
|
|
newHeight = snapTo10(Math.max(20, startHeight + deltaY));
|
|
}
|
|
|
|
setResizeSize({ width: newWidth, height: newHeight });
|
|
});
|
|
};
|
|
|
|
const handleMouseUp = (upEvent: MouseEvent) => {
|
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
|
|
if (rafRef.current) {
|
|
cancelAnimationFrame(rafRef.current);
|
|
rafRef.current = null;
|
|
}
|
|
|
|
const deltaX = upEvent.clientX - startMouseX;
|
|
const deltaY = upEvent.clientY - startMouseY;
|
|
|
|
let newWidth = startWidth;
|
|
let newHeight = startHeight;
|
|
|
|
if (direction === "e" || direction === "se") {
|
|
newWidth = snapTo10(Math.max(50, startWidth + deltaX));
|
|
}
|
|
if (direction === "s" || direction === "se") {
|
|
newHeight = snapTo10(Math.max(20, startHeight + deltaY));
|
|
}
|
|
|
|
// 🆕 탭 컴포넌트 크기 업데이트 먼저 실행
|
|
if (onUpdateComponent) {
|
|
const updatedTabs = tabs.map((tab) => {
|
|
if (tab.id === activeTabId) {
|
|
return {
|
|
...tab,
|
|
components: (tab.components || []).map((c) =>
|
|
c.id === comp.id
|
|
? {
|
|
...c,
|
|
size: {
|
|
width: newWidth,
|
|
height: newHeight,
|
|
},
|
|
}
|
|
: c
|
|
),
|
|
};
|
|
}
|
|
return tab;
|
|
});
|
|
|
|
onUpdateComponent({
|
|
...component,
|
|
componentConfig: {
|
|
...component.componentConfig,
|
|
tabs: updatedTabs,
|
|
},
|
|
});
|
|
}
|
|
|
|
// 🆕 리사이즈 상태 해제 (resizeSize는 마지막 크기 유지, lastResizedCompId 설정)
|
|
setLastResizedCompId(comp.id);
|
|
setResizingCompId(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 || "탭"}
|
|
</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}
|
|
onClick={() => onSelectTabComponent?.(activeTabId, "", {} as TabInlineComponent)}
|
|
>
|
|
{activeTab ? (
|
|
<div
|
|
ref={containerRef}
|
|
className="absolute inset-0 overflow-auto p-2"
|
|
>
|
|
{activeTab.components && activeTab.components.length > 0 ? (
|
|
<div className="relative" style={{ minHeight: "100%", minWidth: "100%" }}>
|
|
{activeTab.components.map((comp: TabInlineComponent) => {
|
|
const isSelected = selectedTabComponentId === comp.id;
|
|
const isDragging = draggingCompId === comp.id;
|
|
const isResizing = resizingCompId === comp.id;
|
|
|
|
// 드래그/리사이즈 중 표시할 크기
|
|
// resizeSize가 있고 해당 컴포넌트이면 resizeSize 우선 사용 (레이아웃 업데이트 반영 전까지)
|
|
const compWidth = comp.size?.width || 200;
|
|
const compHeight = comp.size?.height || 100;
|
|
const isResizingThis = (resizingCompId === comp.id || lastResizedCompId === comp.id) && resizeSize;
|
|
const displayWidth = isResizingThis ? resizeSize!.width : compWidth;
|
|
const displayHeight = isResizingThis ? resizeSize!.height : compHeight;
|
|
|
|
// 컴포넌트 데이터를 DynamicComponentRenderer 형식으로 변환
|
|
const componentData = {
|
|
id: comp.id,
|
|
type: "component" as const,
|
|
componentType: comp.componentType,
|
|
label: comp.label,
|
|
position: comp.position || { x: 0, y: 0 },
|
|
size: { width: displayWidth, height: displayHeight },
|
|
componentConfig: comp.componentConfig || {},
|
|
style: comp.style || {},
|
|
};
|
|
|
|
// 드래그 중인 컴포넌트는 dragPosition 사용, 아니면 저장된 position 사용
|
|
const displayX = isDragging && dragPosition ? dragPosition.x : (comp.position?.x || 0);
|
|
const displayY = isDragging && dragPosition ? dragPosition.y : (comp.position?.y || 0);
|
|
|
|
return (
|
|
<div
|
|
key={comp.id}
|
|
data-tab-comp-id={comp.id}
|
|
className="absolute"
|
|
style={{
|
|
left: displayX,
|
|
top: displayY,
|
|
zIndex: isDragging ? 100 : isSelected ? 10 : 1,
|
|
}}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
console.log("🔍 [탭 컴포넌트] 클릭:", { activeTabId, compId: comp.id, hasOnSelectTabComponent: !!onSelectTabComponent });
|
|
onSelectTabComponent?.(activeTabId, comp.id, comp);
|
|
}}
|
|
>
|
|
{/* 드래그 핸들 - 컴포넌트 외부 상단 */}
|
|
<div
|
|
className={cn(
|
|
"flex h-4 cursor-move items-center justify-between rounded-t border border-b-0 bg-gray-100 px-1",
|
|
isSelected ? "border-primary" : "border-gray-200"
|
|
)}
|
|
style={{ width: comp.size?.width || 200 }}
|
|
onMouseDown={(e) => handleDragStart(e, comp)}
|
|
>
|
|
<div className="flex items-center gap-0.5">
|
|
<Move className="h-2.5 w-2.5 text-gray-400" />
|
|
<span className="text-[9px] text-gray-500 truncate max-w-[100px]">
|
|
{comp.label || comp.componentType}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center">
|
|
<button
|
|
className="rounded p-0.5 hover:bg-gray-200"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onSelectTabComponent?.(activeTabId, comp.id, comp);
|
|
}}
|
|
title="설정"
|
|
>
|
|
<Settings className="h-2.5 w-2.5 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-2.5 w-2.5 text-red-500" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 실제 컴포넌트 렌더링 - 핸들 아래에 별도 영역 */}
|
|
<div
|
|
className={cn(
|
|
"relative rounded-b border bg-white shadow-sm overflow-hidden",
|
|
isSelected
|
|
? "border-primary ring-2 ring-primary/30"
|
|
: "border-gray-200",
|
|
(isDragging || isResizing) && "opacity-80 shadow-lg",
|
|
!(isDragging || isResizing) && "transition-all"
|
|
)}
|
|
style={{
|
|
width: displayWidth,
|
|
height: displayHeight,
|
|
}}
|
|
>
|
|
<div className="h-full w-full pointer-events-none">
|
|
<DynamicComponentRenderer
|
|
component={componentData as any}
|
|
isDesignMode={true}
|
|
formData={{}}
|
|
/>
|
|
</div>
|
|
|
|
{/* 리사이즈 가장자리 영역 - 선택된 컴포넌트에만 표시 */}
|
|
{isSelected && (
|
|
<>
|
|
{/* 오른쪽 가장자리 (너비 조절) */}
|
|
<div
|
|
className="absolute top-0 right-0 w-2 h-full cursor-ew-resize pointer-events-auto z-10 hover:bg-primary/10"
|
|
onMouseDown={(e) => handleResizeStart(e, comp, "e")}
|
|
/>
|
|
{/* 아래 가장자리 (높이 조절) */}
|
|
<div
|
|
className="absolute bottom-0 left-0 w-full h-2 cursor-ns-resize pointer-events-auto z-10 hover:bg-primary/10"
|
|
onMouseDown={(e) => handleResizeStart(e, comp, "s")}
|
|
/>
|
|
{/* 오른쪽 아래 모서리 (너비+높이 조절) */}
|
|
<div
|
|
className="absolute bottom-0 right-0 w-3 h-3 cursor-nwse-resize pointer-events-auto z-20 hover:bg-primary/20"
|
|
onMouseDown={(e) => handleResizeStart(e, comp, "se")}
|
|
/>
|
|
</>
|
|
)}
|
|
</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,
|
|
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: tabs,
|
|
defaultTab: tabsConfig.defaultTab,
|
|
orientation: tabsConfig.orientation || "horizontal",
|
|
variant: tabsConfig.variant || "default",
|
|
allowCloseable: tabsConfig.allowCloseable || false,
|
|
persistSelection: tabsConfig.persistSelection || false,
|
|
};
|
|
|
|
const TabsWidget =
|
|
require("@/components/screen/widgets/TabsWidget").TabsWidget;
|
|
|
|
return (
|
|
<div className="h-full w-full">
|
|
<TabsWidget component={tabsComponent} {...restProps} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 탭 컴포넌트 정의
|
|
*
|
|
* 탭별로 컴포넌트를 자유롭게 배치할 수 있는 레이아웃 컴포넌트
|
|
*/
|
|
ComponentRegistry.registerComponent({
|
|
id: "v2-tabs-widget",
|
|
name: "탭 컴포넌트",
|
|
description:
|
|
"탭별로 컴포넌트를 자유롭게 배치할 수 있는 레이아웃 컴포넌트입니다.",
|
|
category: ComponentCategory.LAYOUT,
|
|
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: "2.0.0",
|
|
|
|
defaultSize: {
|
|
width: 800,
|
|
height: 600,
|
|
},
|
|
|
|
defaultProps: {
|
|
type: "tabs" as const,
|
|
tabs: [
|
|
{
|
|
id: "tab-1",
|
|
label: "탭 1",
|
|
order: 0,
|
|
disabled: false,
|
|
components: [],
|
|
},
|
|
{
|
|
id: "tab-2",
|
|
label: "탭 2",
|
|
order: 1,
|
|
disabled: false,
|
|
components: [],
|
|
},
|
|
] as TabItem[],
|
|
defaultTab: "tab-1",
|
|
orientation: "horizontal" as const,
|
|
variant: "default" as const,
|
|
allowCloseable: false,
|
|
persistSelection: false,
|
|
},
|
|
|
|
// 에디터 모드에서의 렌더링 - 탭 선택 및 컴포넌트 드롭 지원
|
|
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 flex-col overflow-hidden rounded-lg border bg-background"
|
|
onClick={onClick}
|
|
onDragStart={onDragStart}
|
|
onDragEnd={onDragEnd}
|
|
>
|
|
{/* 탭 헤더 */}
|
|
<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 || "탭"}
|
|
</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 }) => {
|
|
return null;
|
|
},
|
|
|
|
// 설정 패널
|
|
configPanel: React.lazy(() =>
|
|
import("@/components/screen/config-panels/TabsConfigPanel").then(
|
|
(module) => ({
|
|
default: module.TabsConfigPanel,
|
|
})
|
|
)
|
|
),
|
|
|
|
// 검증 함수
|
|
validate: (component) => {
|
|
const tabsConfig = (component as any).componentConfig || {};
|
|
const tabs: TabItem[] = tabsConfig.tabs || [];
|
|
const errors: string[] = [];
|
|
|
|
if (!tabs || tabs.length === 0) {
|
|
errors.push("최소 1개 이상의 탭이 필요합니다.");
|
|
}
|
|
|
|
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,
|
|
};
|
|
},
|
|
});
|