리사이즈 기능 추가 및 상태 관리 개선: RealtimePreviewDynamic 및 TabsWidget에서 컴포넌트 리사이즈 기능을 추가하고, 리사이즈 상태를 관리하는 로직을 개선하여 사용자 경험을 향상시켰습니다. 이를 통해 컴포넌트 크기 조정 시 더 나은 반응성과 정확성을 제공하게 되었습니다.
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user