리사이즈 기능 추가 및 상태 관리 개선: 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

@@ -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>
);