리사이즈 기능 추가 및 상태 관리 개선: RealtimePreviewDynamic 및 TabsWidget에서 컴포넌트 리사이즈 기능을 추가하고, 리사이즈 상태를 관리하는 로직을 개선하여 사용자 경험을 향상시켰습니다. 이를 통해 컴포넌트 크기 조정 시 더 나은 반응성과 정확성을 제공하게 되었습니다.

This commit is contained in:
kjs 2026-01-21 09:33:44 +09:00
parent 8cdb8a3047
commit 4781a17b71
3 changed files with 482 additions and 117 deletions

View File

@ -39,6 +39,7 @@ interface RealtimePreviewProps {
onUpdateComponent?: (updatedComponent: any) => void; // 🆕 컴포넌트 업데이트 콜백
onSelectTabComponent?: (tabId: string, compId: string, comp: any) => void; // 🆕 탭 내부 컴포넌트 선택 콜백
selectedTabComponentId?: string; // 🆕 선택된 탭 컴포넌트 ID
onResize?: (componentId: string, newSize: { width: number; height: number }) => void; // 🆕 리사이즈 콜백
// 버튼 액션을 위한 props
screenId?: number;
@ -139,6 +140,7 @@ const RealtimePreviewDynamicComponent: React.FC<RealtimePreviewProps> = ({
onUpdateComponent, // 🆕 컴포넌트 업데이트 콜백
onSelectTabComponent, // 🆕 탭 내부 컴포넌트 선택 콜백
selectedTabComponentId, // 🆕 선택된 탭 컴포넌트 ID
onResize, // 🆕 리사이즈 콜백
}) => {
// 🆕 화면 다국어 컨텍스트
const { getTranslatedText } = useScreenMultiLang();
@ -146,6 +148,102 @@ const RealtimePreviewDynamicComponent: React.FC<RealtimePreviewProps> = ({
const [actualHeight, setActualHeight] = React.useState<number | null>(null);
const contentRef = React.useRef<HTMLDivElement>(null);
const lastUpdatedHeight = React.useRef<number | null>(null);
// 🆕 리사이즈 상태
const [isResizing, setIsResizing] = React.useState(false);
const [resizeSize, setResizeSize] = React.useState<{ width: number; height: number } | null>(null);
const rafRef = React.useRef<number | null>(null);
// 🆕 size가 업데이트되면 resizeSize 초기화 (레이아웃 상태가 props에 반영되었음)
React.useEffect(() => {
if (resizeSize && !isResizing) {
// component.size가 resizeSize와 같아지면 resizeSize 초기화
if (component.size?.width === resizeSize.width && component.size?.height === resizeSize.height) {
setResizeSize(null);
}
}
}, [component.size?.width, component.size?.height, resizeSize, isResizing]);
// 10px 단위 스냅 함수
const snapTo10 = (value: number) => Math.round(value / 10) * 10;
// 🆕 리사이즈 핸들러
const handleResizeStart = React.useCallback(
(e: React.MouseEvent, direction: "e" | "s" | "se") => {
e.stopPropagation();
e.preventDefault();
const startMouseX = e.clientX;
const startMouseY = e.clientY;
const startWidth = component.size?.width || 200;
const startHeight = component.size?.height || 100;
setIsResizing(true);
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));
}
// 🆕 리사이즈 상태는 유지한 채로 크기 변경 콜백 호출
// resizeSize는 null로 설정하지 않고 마지막 크기 유지
// (component.size가 업데이트되면 자연스럽게 올바른 크기 표시)
// 🆕 크기 변경 콜백 호출하여 레이아웃 상태 업데이트
if (onResize) {
onResize(component.id, { width: newWidth, height: newHeight });
}
// 🆕 리사이즈 플래그만 해제 (resizeSize는 마지막 크기 유지)
setIsResizing(false);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
},
[component.id, component.size, onResize]
);
// 플로우 위젯의 실제 높이 측정
React.useEffect(() => {
@ -249,18 +347,27 @@ const RealtimePreviewDynamicComponent: React.FC<RealtimePreviewProps> = ({
return `${actualHeight}px`;
}
// 1순위: style.height가 있으면 우선 사용 (문자열 그대로 또는 숫자+px)
// 🆕 1순위: size.height가 있으면 우선 사용 (레이아웃에서 관리되는 실제 크기)
// size는 레이아웃 상태에서 직접 관리되며 리사이즈로 변경됨
if (size?.height && size.height > 0) {
if (component.componentConfig?.type === "table-list") {
return `${Math.max(size.height, 200)}px`;
}
return `${size.height}px`;
}
// 2순위: componentStyle.height (컴포넌트 정의에서 온 기본 스타일)
if (componentStyle?.height) {
return typeof componentStyle.height === "number" ? `${componentStyle.height}px` : componentStyle.height;
}
// 2순위: size.height (픽셀)
// 3순위: 기본값
if (component.componentConfig?.type === "table-list") {
return `${Math.max(size?.height || 200, 200)}px`;
return "200px";
}
// size.height가 있으면 그대로 사용, 없으면 최소 10px
return `${size?.height || 10}px`;
// 기본 높이
return "10px";
};
// layout 타입 컴포넌트인지 확인
@ -405,16 +512,22 @@ const RealtimePreviewDynamicComponent: React.FC<RealtimePreviewProps> = ({
const { adjustedPositionX, isOnSplitPanel, isDraggingSplitPanel } = calculateButtonPosition();
// 🆕 리사이즈 크기가 있으면 우선 사용
// (size가 업데이트되면 위 useEffect에서 resizeSize를 null로 설정)
const displayWidth = resizeSize ? `${resizeSize.width}px` : getWidth();
const displayHeight = resizeSize ? `${resizeSize.height}px` : getHeight();
const baseStyle = {
left: `${adjustedPositionX}px`, // 🆕 조정된 X 좌표 사용
top: `${position.y}px`,
...componentStyle, // componentStyle 전체 적용 (DynamicComponentRenderer에서 이미 size가 변환됨)
width: getWidth(), // getWidth() 우선 (table-list 등 특수 케이스)
height: getHeight(), // getHeight() 우선 (flow-widget 등 특수 케이스)
width: displayWidth, // 🆕 리사이즈 중이면 resizeSize 사용
height: displayHeight, // 🆕 리사이즈 중이면 resizeSize 사용
zIndex: component.type === "layout" ? 1 : position.z || 2,
right: undefined,
// 🆕 분할 패널 드래그 중에는 트랜지션 없이 즉시 이동
// 🆕 분할 패널 드래그 중에는 트랜지션 없이 즉시 이동, 리사이즈 중에도 트랜지션 없음
transition:
isResizing ? "none" :
isOnSplitPanel && isButtonComponent ? (isDraggingSplitPanel ? "none" : "left 0.1s ease-out") : undefined,
};
@ -546,6 +659,27 @@ const RealtimePreviewDynamicComponent: React.FC<RealtimePreviewProps> = ({
)}
</div>
)}
{/* 🆕 리사이즈 가장자리 영역 - 선택된 컴포넌트 + 디자인 모드에서만 표시 */}
{isSelected && isDesignMode && onResize && (
<>
{/* 오른쪽 가장자리 (너비 조절) */}
<div
className="absolute top-0 right-0 w-2 h-full cursor-ew-resize z-20 hover:bg-primary/10"
onMouseDown={(e) => handleResizeStart(e, "e")}
/>
{/* 아래 가장자리 (높이 조절) */}
<div
className="absolute bottom-0 left-0 w-full h-2 cursor-ns-resize z-20 hover:bg-primary/10"
onMouseDown={(e) => handleResizeStart(e, "s")}
/>
{/* 오른쪽 아래 모서리 (너비+높이 조절) */}
<div
className="absolute bottom-0 right-0 w-3 h-3 cursor-nwse-resize z-30 hover:bg-primary/20"
onMouseDown={(e) => handleResizeStart(e, "se")}
/>
</>
)}
</div>
);
};

View File

@ -4905,115 +4905,155 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
</TabsContent>
<TabsContent value="properties" className="mt-0 flex-1 overflow-hidden">
{/* 🆕 탭 내부 컴포넌트가 선택된 경우 별도 패널 표시 */}
{/* 탭 내부 컴포넌트 선택 시에도 UnifiedPropertiesPanel 사용 */}
{selectedTabComponentInfo ? (
<div className="flex h-full flex-col p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<h3 className="text-sm font-semibold"> </h3>
<p className="text-xs text-muted-foreground">
{selectedTabComponentInfo.component.label || selectedTabComponentInfo.component.componentType}
</p>
</div>
<button
className="text-xs text-muted-foreground hover:text-foreground"
onClick={() => setSelectedTabComponentInfo(null)}
>
</button>
</div>
{/* DynamicComponentConfigPanel 렌더링 */}
<div className="flex-1 overflow-auto">
{(() => {
const DynamicConfigPanel = require("@/lib/utils/getComponentConfigPanel").DynamicComponentConfigPanel;
const tabComp = selectedTabComponentInfo.component;
// 탭 내부 컴포넌트를 일반 컴포넌트 형식으로 변환
const componentForConfig = {
id: tabComp.id,
type: "component",
componentType: tabComp.componentType,
label: tabComp.label,
position: tabComp.position,
size: tabComp.size,
componentConfig: tabComp.componentConfig || {},
style: tabComp.style || {},
inputType: tabComp.inputType || tabComp.componentConfig?.inputType, // 🆕 inputType 추가
widgetType: tabComp.widgetType || tabComp.componentConfig?.widgetType, // 🆕 widgetType 추가
(() => {
const tabComp = selectedTabComponentInfo.component;
// 탭 내부 컴포넌트를 ComponentData 형식으로 변환
const tabComponentAsComponentData: ComponentData = {
id: tabComp.id,
type: "component",
componentType: tabComp.componentType,
label: tabComp.label,
position: tabComp.position || { x: 0, y: 0 },
size: tabComp.size || { width: 200, height: 100 },
componentConfig: tabComp.componentConfig || {},
style: tabComp.style || {},
} as ComponentData;
// 탭 내부 컴포넌트용 속성 업데이트 핸들러
const updateTabComponentProperty = (componentId: string, path: string, value: any) => {
const { tabsComponentId, tabId } = selectedTabComponentInfo;
setLayout((prevLayout) => {
const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId);
if (!tabsComponent) return prevLayout;
const currentConfig = (tabsComponent as any).componentConfig || {};
const tabs = currentConfig.tabs || [];
const updatedTabs = tabs.map((tab: any) => {
if (tab.id === tabId) {
return {
...tab,
components: (tab.components || []).map((comp: any) => {
if (comp.id !== componentId) return comp;
// path를 파싱하여 중첩 속성 업데이트
const pathParts = path.split(".");
const newComp = { ...comp };
let current: any = newComp;
for (let i = 0; i < pathParts.length - 1; i++) {
const part = pathParts[i];
if (!current[part]) {
current[part] = {};
} else {
current[part] = { ...current[part] };
}
current = current[part];
}
current[pathParts[pathParts.length - 1]] = value;
return newComp;
}),
};
}
return tab;
});
const updatedComponent = {
...tabsComponent,
componentConfig: { ...currentConfig, tabs: updatedTabs },
};
return (
<DynamicConfigPanel
componentId={tabComp.componentType}
component={componentForConfig}
config={tabComp.componentConfig || {}}
screenTableName={selectedScreen?.tableName}
tableColumns={tables.length > 0 ? tables[0].columns : []}
menuObjid={selectedScreen?.menuObjid}
currentComponent={componentForConfig}
onChange={(newConfig: any) => {
// componentConfig 전체 업데이트 - 함수형 업데이트로 클로저 문제 해결
const { tabsComponentId, tabId, componentId } = selectedTabComponentInfo;
setLayout((prevLayout) => {
const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId);
if (!tabsComponent) return prevLayout;
// 선택된 컴포넌트 정보 업데이트
const updatedComp = updatedTabs
.find((t: any) => t.id === tabId)
?.components?.find((c: any) => c.id === componentId);
if (updatedComp) {
setSelectedTabComponentInfo((prev) =>
prev ? { ...prev, component: updatedComp } : null
);
}
const currentConfig = (tabsComponent as any).componentConfig || {};
const tabs = currentConfig.tabs || [];
return {
...prevLayout,
components: prevLayout.components.map((c) =>
c.id === tabsComponentId ? updatedComponent : c
),
};
});
};
const updatedTabs = tabs.map((tab: any) => {
if (tab.id === tabId) {
return {
...tab,
components: (tab.components || []).map((comp: any) =>
comp.id === componentId
? { ...comp, componentConfig: newConfig }
: comp
),
};
}
return tab;
});
// 탭 내부 컴포넌트 삭제 핸들러
const deleteTabComponent = (componentId: string) => {
const { tabsComponentId, tabId } = selectedTabComponentInfo;
setLayout((prevLayout) => {
const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId);
if (!tabsComponent) return prevLayout;
const updatedComponent = {
...tabsComponent,
componentConfig: {
...currentConfig,
tabs: updatedTabs,
},
};
const currentConfig = (tabsComponent as any).componentConfig || {};
const tabs = currentConfig.tabs || [];
const newLayout = {
...prevLayout,
components: prevLayout.components.map((c) =>
c.id === tabsComponentId ? updatedComponent : c
),
};
const updatedTabs = tabs.map((tab: any) => {
if (tab.id === tabId) {
return {
...tab,
components: (tab.components || []).filter((c: any) => c.id !== componentId),
};
}
return tab;
});
// 선택된 컴포넌트 정보 업데이트
const updatedComp = updatedTabs
.find((t: any) => t.id === tabId)
?.components?.find((c: any) => c.id === componentId);
if (updatedComp) {
setSelectedTabComponentInfo((prev) =>
prev ? { ...prev, component: updatedComp } : null
);
}
const updatedComponent = {
...tabsComponent,
componentConfig: { ...currentConfig, tabs: updatedTabs },
};
return newLayout;
});
}}
screenTableName={selectedScreen?.tableName}
tableColumns={tables.length > 0 ? tables[0].columns : []}
setSelectedTabComponentInfo(null);
return {
...prevLayout,
components: prevLayout.components.map((c) =>
c.id === tabsComponentId ? updatedComponent : c
),
};
});
};
return (
<div className="flex h-full flex-col">
<div className="flex items-center justify-between border-b px-4 py-2">
<span className="text-xs text-muted-foreground"> </span>
<button
className="text-xs text-muted-foreground hover:text-foreground"
onClick={() => setSelectedTabComponentInfo(null)}
>
</button>
</div>
<div className="flex-1 overflow-hidden">
<UnifiedPropertiesPanel
selectedComponent={tabComponentAsComponentData}
tables={tables}
onUpdateProperty={updateTabComponentProperty}
onDeleteComponent={deleteTabComponent}
currentTable={tables.length > 0 ? tables[0] : undefined}
currentTableName={selectedScreen?.tableName}
currentScreenCompanyCode={selectedScreen?.companyCode}
onStyleChange={(style) => {
updateTabComponentProperty(tabComp.id, "style", style);
}}
allComponents={layout.components}
menuObjid={selectedScreen?.menuObjid}
menuObjid={menuObjid}
/>
);
})()}
</div>
</div>
</div>
</div>
);
})()
) : (
<UnifiedPropertiesPanel
selectedComponent={selectedComponent || undefined}
@ -5377,6 +5417,25 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
setLayout(newLayout);
saveToHistory(newLayout);
}}
// 🆕 리사이즈 핸들러 (10px 스냅 적용됨)
onResize={(componentId, newSize) => {
setLayout((prevLayout) => {
const updatedComponents = prevLayout.components.map((comp) =>
comp.id === componentId
? { ...comp, size: newSize }
: comp
);
const newLayout = {
...prevLayout,
components: updatedComponents,
};
// saveToHistory는 별도로 호출 (prevLayout 기반)
setTimeout(() => saveToHistory(newLayout), 0);
return newLayout;
});
}}
// 🆕 탭 내부 컴포넌트 선택 핸들러
onSelectTabComponent={(tabId, compId, comp) =>
handleSelectTabComponent(component.id, tabId, compId, comp)
@ -5476,6 +5535,24 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
// console.log("📤 자식 컴포넌트 설정 변경을 상세설정에 알림:", config);
// TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요
}}
// 🆕 자식 컴포넌트 리사이즈 핸들러
onResize={(componentId, newSize) => {
setLayout((prevLayout) => {
const updatedComponents = prevLayout.components.map((comp) =>
comp.id === componentId
? { ...comp, size: newSize }
: comp
);
const newLayout = {
...prevLayout,
components: updatedComponents,
};
setTimeout(() => saveToHistory(newLayout), 0);
return newLayout;
});
}}
/>
);
})}

View File

@ -1,6 +1,6 @@
"use client";
import React, { useState, useRef, useCallback } from "react";
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";
@ -21,8 +21,26 @@ const TabsDesignEditor: React.FC<{
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;
@ -157,6 +175,110 @@ const TabsDesignEditor: React.FC<{
[activeTabId, component, onUpdateComponent, tabs]
);
// 10px 단위 스냅 함수
const snapTo10 = useCallback((value: number) => Math.round(value / 10) * 10, []);
// 리사이즈 시작 핸들러
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">
{/* 탭 헤더 */}
@ -205,6 +327,15 @@ const TabsDesignEditor: React.FC<{
{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 = {
@ -213,7 +344,7 @@ const TabsDesignEditor: React.FC<{
componentType: comp.componentType,
label: comp.label,
position: comp.position || { x: 0, y: 0 },
size: comp.size || { width: 200, height: 100 },
size: { width: displayWidth, height: displayHeight },
componentConfig: comp.componentConfig || {},
style: comp.style || {},
};
@ -279,23 +410,46 @@ const TabsDesignEditor: React.FC<{
{/* 실제 컴포넌트 렌더링 - 핸들 아래에 별도 영역 */}
<div
className={cn(
"rounded-b border bg-white shadow-sm overflow-hidden pointer-events-none",
"relative rounded-b border bg-white shadow-sm overflow-hidden",
isSelected
? "border-primary ring-2 ring-primary/30"
: "border-gray-200",
isDragging && "opacity-80 shadow-lg",
!isDragging && "transition-all"
(isDragging || isResizing) && "opacity-80 shadow-lg",
!(isDragging || isResizing) && "transition-all"
)}
style={{
width: comp.size?.width || 200,
height: comp.size?.height || 100,
width: displayWidth,
height: displayHeight,
}}
>
<DynamicComponentRenderer
component={componentData as any}
isDesignMode={true}
formData={{}}
/>
<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>
);