드래그 앤 드롭 기능 개선 및 Unified 컴포넌트 매핑 추가: ScreenDesigner, TabsWidget, DynamicComponentRenderer에서 드래그 앤 드롭 시 컴포넌트의 위치와 크기를 최적화하고, Unified 컴포넌트에 대한 매핑 로직을 추가하여 사용자 경험을 향상시켰습니다. 또한, ButtonConfigPanel에서 컴포넌트가 없는 경우 방어 처리 로직을 추가하여 안정성을 높였습니다.
This commit is contained in:
@@ -18,8 +18,9 @@ const TabsDesignEditor: React.FC<{
|
||||
}> = ({ 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 [dragPosition, setDragPosition] = useState<{ x: number; y: number } | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const rafRef = useRef<number | null>(null);
|
||||
|
||||
const activeTab = tabs.find((t) => t.id === activeTabId);
|
||||
|
||||
@@ -65,64 +66,54 @@ const TabsDesignEditor: React.FC<{
|
||||
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;
|
||||
// 드래그 시작 시 마우스 위치와 컴포넌트의 현재 위치 저장
|
||||
const startMouseX = e.clientX;
|
||||
const startMouseY = e.clientY;
|
||||
const startLeft = comp.position?.x || 0;
|
||||
const startTop = comp.position?.y || 0;
|
||||
|
||||
setDraggingCompId(comp.id);
|
||||
setDragOffset({ x: offsetX, y: offsetY });
|
||||
setDragPosition({ x: startLeft, y: startTop });
|
||||
|
||||
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`;
|
||||
// requestAnimationFrame으로 성능 최적화
|
||||
if (rafRef.current) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
}
|
||||
|
||||
rafRef.current = requestAnimationFrame(() => {
|
||||
// 마우스 이동량 계산
|
||||
const deltaX = moveEvent.clientX - startMouseX;
|
||||
const deltaY = moveEvent.clientY - startMouseY;
|
||||
|
||||
// 새 위치 = 시작 위치 + 이동량
|
||||
const newX = Math.max(0, startLeft + deltaX);
|
||||
const newY = Math.max(0, startTop + deltaY);
|
||||
|
||||
// React 상태로 위치 업데이트 (리렌더링 트리거)
|
||||
setDragPosition({ x: newX, y: newY });
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseUp = (upEvent: MouseEvent) => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
setDraggingCompId(null);
|
||||
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
if (rafRef.current) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
rafRef.current = null;
|
||||
}
|
||||
|
||||
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;
|
||||
// 마우스 이동량 계산
|
||||
const deltaX = upEvent.clientX - startMouseX;
|
||||
const deltaY = upEvent.clientY - startMouseY;
|
||||
|
||||
// 새 위치 = 시작 위치 + 이동량
|
||||
const newX = Math.max(0, startLeft + deltaX);
|
||||
const newY = Math.max(0, startTop + deltaY);
|
||||
|
||||
setDraggingCompId(null);
|
||||
setDragPosition(null);
|
||||
|
||||
// 탭 컴포넌트 위치 업데이트
|
||||
if (onUpdateComponent) {
|
||||
@@ -198,7 +189,6 @@ const TabsDesignEditor: React.FC<{
|
||||
|
||||
{/* 탭 컨텐츠 영역 - 드롭 영역 */}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative flex-1 overflow-hidden"
|
||||
data-tabs-container="true"
|
||||
data-component-id={component.id}
|
||||
@@ -206,9 +196,12 @@ const TabsDesignEditor: React.FC<{
|
||||
onClick={() => onSelectTabComponent?.(activeTabId, "", {} as TabInlineComponent)}
|
||||
>
|
||||
{activeTab ? (
|
||||
<div className="absolute inset-0 overflow-auto p-2">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="absolute inset-0 overflow-auto p-2"
|
||||
>
|
||||
{activeTab.components && activeTab.components.length > 0 ? (
|
||||
<div className="relative h-full w-full">
|
||||
<div className="relative" style={{ minHeight: "100%", minWidth: "100%" }}>
|
||||
{activeTab.components.map((comp: TabInlineComponent) => {
|
||||
const isSelected = selectedTabComponentId === comp.id;
|
||||
const isDragging = draggingCompId === comp.id;
|
||||
@@ -225,22 +218,18 @@ const TabsDesignEditor: React.FC<{
|
||||
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={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"
|
||||
)}
|
||||
className="absolute"
|
||||
style={{
|
||||
left: comp.position?.x || 0,
|
||||
top: comp.position?.y || 0,
|
||||
width: comp.size?.width || 200,
|
||||
height: comp.size?.height || 100,
|
||||
left: displayX,
|
||||
top: displayY,
|
||||
zIndex: isDragging ? 100 : isSelected ? 10 : 1,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
@@ -248,18 +237,22 @@ const TabsDesignEditor: React.FC<{
|
||||
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"
|
||||
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-1">
|
||||
<Move className="h-3 w-3 text-gray-400" />
|
||||
<span className="text-[10px] text-gray-500 truncate max-w-[120px]">
|
||||
<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 gap-1">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className="rounded p-0.5 hover:bg-gray-200"
|
||||
onClick={(e) => {
|
||||
@@ -268,7 +261,7 @@ const TabsDesignEditor: React.FC<{
|
||||
}}
|
||||
title="설정"
|
||||
>
|
||||
<Settings className="h-3 w-3 text-gray-500" />
|
||||
<Settings className="h-2.5 w-2.5 text-gray-500" />
|
||||
</button>
|
||||
<button
|
||||
className="rounded p-0.5 hover:bg-red-100"
|
||||
@@ -278,13 +271,26 @@ const TabsDesignEditor: React.FC<{
|
||||
}}
|
||||
title="삭제"
|
||||
>
|
||||
<Trash2 className="h-3 w-3 text-red-500" />
|
||||
<Trash2 className="h-2.5 w-2.5 text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 실제 컴포넌트 렌더링 */}
|
||||
<div className="h-full w-full pt-5 overflow-hidden pointer-events-none">
|
||||
{/* 실제 컴포넌트 렌더링 - 핸들 아래에 별도 영역 */}
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-b border bg-white shadow-sm overflow-hidden pointer-events-none",
|
||||
isSelected
|
||||
? "border-primary ring-2 ring-primary/30"
|
||||
: "border-gray-200",
|
||||
isDragging && "opacity-80 shadow-lg",
|
||||
!isDragging && "transition-all"
|
||||
)}
|
||||
style={{
|
||||
width: comp.size?.width || 200,
|
||||
height: comp.size?.height || 100,
|
||||
}}
|
||||
>
|
||||
<DynamicComponentRenderer
|
||||
component={componentData as any}
|
||||
isDesignMode={true}
|
||||
|
||||
Reference in New Issue
Block a user