From 446c7b68713e2b4897e308b761afae37f8123882 Mon Sep 17 00:00:00 2001 From: "DESKTOP-4N4R3LH\\jqk17" Date: Sat, 21 Feb 2026 10:51:50 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8=20feat:=20Portal=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=EC=9C=BC=EB=A1=9C=20=EC=98=A4=EB=B2=84=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=A5=BC=20DOM=20=ED=8A=B8=EB=A6=AC=20=EB=B0=96?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/editor/src/components/editor/Canvas.tsx | 6 + packages/ui/src/core/EditorNodeWrapper.tsx | 12 +- packages/ui/src/core/SelectionOverlay.tsx | 133 +++++++++++++++++++ 3 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 packages/ui/src/core/SelectionOverlay.tsx diff --git a/apps/editor/src/components/editor/Canvas.tsx b/apps/editor/src/components/editor/Canvas.tsx index ee8fac2..f461692 100644 --- a/apps/editor/src/components/editor/Canvas.tsx +++ b/apps/editor/src/components/editor/Canvas.tsx @@ -20,6 +20,7 @@ import handleWheel from "@/utils/editor/handleWheel"; import { DragProvider } from "@repo/ui/context/dragContext"; import EditorNodeWrapper from "@repo/ui/core/EditorNodeWrapper"; import NodeRenderer from "@repo/ui/core/NodeRenderer"; +import SelectionOverlay from "@repo/ui/core/SelectionOverlay"; import { WcxNode } from "@repo/ui/types/nodes"; import React, { useRef } from "react"; @@ -134,6 +135,11 @@ export default function Canvas() { {renderTree({ id: null })} + {/* 포탈 기반 선택 오버레이 — 노드 DOM 트리 바깥에서 렌더링 */} + diff --git a/packages/ui/src/core/EditorNodeWrapper.tsx b/packages/ui/src/core/EditorNodeWrapper.tsx index f256e78..6ac3398 100644 --- a/packages/ui/src/core/EditorNodeWrapper.tsx +++ b/packages/ui/src/core/EditorNodeWrapper.tsx @@ -1,7 +1,7 @@ //에디터 모드전용 노드 렌더러 래퍼 컴포넌트 import clsx from "clsx"; import { useDragStore } from "context/dragContext"; -import { useRef, useState } from "react"; +import { useState } from "react"; import { Rnd } from "react-rnd"; import { WcxNode } from "types"; import { CanvasState, Layer } from "types/rnd"; @@ -52,8 +52,9 @@ export default function EditorNodeWrapper({ const { id } = node; const { width, height, x, y, zIndex } = node.layout; const selectedNodeGuideClasses = { - handle: "bg-white border-2 rounded-full border-rnd-handle !w-2 !h-2 ", - outline: "ring ring-2 ring-rnd-handle", + // 시각적 핸들은 SelectionOverlay 포탈에서 렌더링. + // Rnd의 핸들은 인터랙션만 담당 (투명하게 유지) + handle: "opacity-0 !w-3 !h-3 ", }; // 필요한 데이터만 구독 @@ -198,10 +199,7 @@ export default function EditorNodeWrapper({ selectNode(id); }} style={wrapperStyle} - className={clsx( - "relative h-full w-full transition-shadow duration-200", - isSelected && selectedNodeGuideClasses.outline, - )} + className="relative h-full w-full" > {children} diff --git a/packages/ui/src/core/SelectionOverlay.tsx b/packages/ui/src/core/SelectionOverlay.tsx new file mode 100644 index 0000000..9b81560 --- /dev/null +++ b/packages/ui/src/core/SelectionOverlay.tsx @@ -0,0 +1,133 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { CanvasState } from "types/rnd"; + +interface SelectionOverlayProps { + selectedNodeId: string | null; + canvas: CanvasState; +} + +interface OverlayRect { + x: number; + y: number; + width: number; + height: number; +} + +/** + * 포탈 기반 선택 오버레이 컴포넌트 + * + * 노드 DOM 트리 바깥에 렌더링되어 부모의 overflow:hidden 등에 의해 + * 선택 테두리가 잘리는 문제를 근본적으로 해결합니다. + * + * - data-component-id 속성으로 대상 노드 DOM 요소를 탐색 + * - getBoundingClientRect()로 화면 좌표를 얻어 캔버스 좌표계로 변환 + * - ResizeObserver로 크기/위치 변화를 실시간 감지 + */ +export default function SelectionOverlay({ + selectedNodeId, + canvas, +}: SelectionOverlayProps) { + const [rect, setRect] = useState(null); + const containerRef = useRef(null); + const rafRef = useRef(0); + + useEffect(() => { + if (!selectedNodeId) { + setRect(null); + return; + } + + // 선택된 노드의 DOM 요소 찾기 (Rnd 래퍼가 아닌 내부 컴포넌트) + const nodeEl = document.querySelector( + `[data-component-id="${selectedNodeId}"]`, + ) as HTMLElement | null; + + if (!nodeEl) { + setRect(null); + return; + } + + function updateRect() { + if (!nodeEl || !containerRef.current) return; + + const containerRect = containerRef.current.getBoundingClientRect(); + const nodeRect = nodeEl.getBoundingClientRect(); + + // 컨테이너(포탈 레이어) 기준 상대 좌표로 변환 + // 포탈 레이어는 캔버스 transform 안에 있으므로 scale이 이미 적용됨. + // getBoundingClientRect()는 scale이 적용된 화면 좌표를 반환하므로, + // 캔버스 좌표계로 변환하려면 scale로 나눠야 함. + setRect({ + x: (nodeRect.left - containerRect.left) / canvas.scale, + y: (nodeRect.top - containerRect.top) / canvas.scale, + width: nodeRect.width / canvas.scale, + height: nodeRect.height / canvas.scale, + }); + } + + // 초기 위치 계산 (다음 프레임에서 실행하여 렌더링 완료 보장) + rafRef.current = requestAnimationFrame(updateRect); + + // ResizeObserver로 크기/위치 변화 감지 + const resizeObserver = new ResizeObserver(() => { + cancelAnimationFrame(rafRef.current); + rafRef.current = requestAnimationFrame(updateRect); + }); + resizeObserver.observe(nodeEl); + + // MutationObserver로 style/transform 변화 감지 (드래그 시) + const mutationObserver = new MutationObserver(() => { + cancelAnimationFrame(rafRef.current); + rafRef.current = requestAnimationFrame(updateRect); + }); + + // Rnd 래퍼(부모)의 transform 변화를 감지하기 위해 부모 요소도 관찰 + const rndWrapper = nodeEl.closest(".react-draggable") as HTMLElement | null; + if (rndWrapper) { + mutationObserver.observe(rndWrapper, { + attributes: true, + attributeFilter: ["style", "class"], + }); + } + + // nodeEl 자체의 변화도 관찰 + mutationObserver.observe(nodeEl, { + attributes: true, + attributeFilter: ["style", "class"], + }); + + return () => { + cancelAnimationFrame(rafRef.current); + resizeObserver.disconnect(); + mutationObserver.disconnect(); + }; + }, [selectedNodeId, canvas.scale]); + + return ( +
+ {rect && selectedNodeId && ( +
+ {/* 리사이즈 핸들 (4 corners) — 시각적 표시 전용, 인터랙션은 Rnd가 담당 */} +
+
+
+
+
+ )} +
+ ); +} From a354a653ed642799154dcf80ecd7f3f04be6d339 Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 25 Feb 2026 12:55:08 +0900 Subject: [PATCH 2/5] fix: Stack item resize-then-drag jump bug react-rnd accumulates internal position during resize. !transform-none hides it visually, but when drag starts, isTransformActive becomes true and the accumulated value applies all at once causing the node to jump. Fix: call rndRef.current?.updatePosition on onResizeStop to reset react-rnd internal state for relative-positioned nodes. --- packages/ui/src/core/EditorNodeWrapper.tsx | 51 +++++++++++++++------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/core/EditorNodeWrapper.tsx b/packages/ui/src/core/EditorNodeWrapper.tsx index 6ac3398..e52114d 100644 --- a/packages/ui/src/core/EditorNodeWrapper.tsx +++ b/packages/ui/src/core/EditorNodeWrapper.tsx @@ -1,7 +1,8 @@ //에디터 모드전용 노드 렌더러 래퍼 컴포넌트 import clsx from "clsx"; import { useDragStore } from "context/dragContext"; -import { useState } from "react"; +import { useRef, useState } from "react"; +import { flushSync } from "react-dom"; import { Rnd } from "react-rnd"; import { WcxNode } from "types"; import { CanvasState, Layer } from "types/rnd"; @@ -40,6 +41,7 @@ export default function EditorNodeWrapper({ cursor: "move", }; + const rndRef = useRef(null); const [isTransformActive, setIsTransformActive] = useState(false); const [dragPosition, setDragPosition] = useState<{ x: number; @@ -96,31 +98,45 @@ export default function EditorNodeWrapper({ return ( { e.stopPropagation(); setDraggingId(id); //드래그 시작 알림 + console.log(d.node.offsetLeft); if (hasRelativePosition) { const { offsetLeft, offsetTop } = d.node; - updateNode(id, { x: offsetLeft, y: offsetTop }); - setDragPosition({ x: offsetLeft, y: offsetTop }); + setIsTransformActive(true); console.log( - `좌표 보정 작동 offsetLeft - ${offsetLeft} // offsetTop - ${offsetTop} `, + `[dragStart]_현재 추출된 노드 좌표 offsetLeft - ${offsetLeft} // offsetTop - ${offsetTop} `, ); + flushSync(() => { + updateNode(id, { x: offsetLeft, y: offsetTop }); + setDragPosition({ x: offsetLeft, y: offsetTop }); + }); } + console.log( + `[dragStart]현재 노드의 실제 렌더링position - x:${x}, y:${y}`, + ); }} //TODO-이동중에 로직 실행하면 성능상 부담이 될 수 있다... 최적화 고민 해보기 onDrag={(e, d) => { @@ -132,18 +148,15 @@ export default function EditorNodeWrapper({ }} onDragStop={(e, d) => { setIsTransformActive(false); - console.log(`현재 노드 ${id}- 포지션 ${node.style.position}`); + console.log(`[dragStop]현재 노드 ${id}- 포지션 ${node.style.position}`); const stackId = findStackId(e); setDraggingId(null); setHoveredStackId(null); - console.log( - `드래그 종료시 노드의 좌표 x:${d.node.offsetLeft}, y:${d.node.offsetTop}`, - ); + console.log(`[dragStop]노드의 좌표 x:${x}, y:${y}`); if (hasRelativePosition) { - console.log("relative position"); return; } @@ -169,13 +182,21 @@ export default function EditorNodeWrapper({ }) } */ - onResizeStop={(e, dir, ref, delta, pos) => + onResizeStop={(e, dir, ref, delta, pos) => { updateNode(id, { width: parseInt(ref.style.width), height: parseInt(ref.style.height), ...(hasRelativePosition ? {} : pos), - }) - } + }); + // [버그 수정] react-rnd는 리사이즈 중 top/left 방향 핸들 사용 시 + // 내부 position을 누적 변경한다. !transform-none으로 DOM에선 보이지 않지만 + // 드래그 시작 시 isTransformActive가 true로 바뀌면서 누적된 값이 한꺼번에 반영되어 + // 노드가 튀는 버그가 발생한다. → 리사이즈 종료 시 내부 position을 {x:0, y:0}으로 강제 동기화. + if (hasRelativePosition) { + setDragPosition({ x: 0, y: 0 }); + rndRef.current?.updatePosition({ x: 0, y: 0 }); + } + }} enableResizing={isGroup ? undefined : isSelected ? undefined : false} disableDragging={!isSelected} resizeHandleClasses={{ From 136a0a261552bdfca2cbe892211d7738f4e2171f Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 25 Feb 2026 14:45:50 +0900 Subject: [PATCH 3/5] docs: Stack item resize-then-drag jump bug troubleshooting Add troubleshooting doc for react-rnd internal position accumulation bug. - Root cause: react-rnd accumulates internal position during resize, which is hidden by !transform-none but applied on drag start - Fix: call rndRef.current?.updatePosition at the top of onDragStart, before setIsTransformActive(true) to reset internal lib state --- .../stack-item-resize-drag-jump.md | 189 ++++++++++++++++++ packages/ui/src/core/EditorNodeWrapper.tsx | 18 +- 2 files changed, 199 insertions(+), 8 deletions(-) create mode 100644 .notes/troubleshooting/stack-item-resize-drag-jump.md diff --git a/.notes/troubleshooting/stack-item-resize-drag-jump.md b/.notes/troubleshooting/stack-item-resize-drag-jump.md new file mode 100644 index 0000000..3cba1af --- /dev/null +++ b/.notes/troubleshooting/stack-item-resize-drag-jump.md @@ -0,0 +1,189 @@ +# 트러블슈팅: Stack Item 리사이즈 후 드래그 시 노드 점프 버그 + +> **발생일**: 2026-02-25 +> **파일**: `packages/ui/src/core/EditorNodeWrapper.tsx` +> **라이브러리**: `react-rnd@10.5.2` + +--- + +## 1. 버그 증상 + +Stack 내부의 `position: relative` 아이템 노드를 **리사이즈한 후 드래그를 시작하면**, 노드가 엉뚱한 위치로 순간이동(점프)하는 현상이 발생했다. + +- 리사이즈 중에는 정상적으로 flexbox 정렬을 따름 +- 드래그를 시작하는 순간만 점프 발생 +- 리사이즈 없이 바로 드래그하면 정상 동작 + +--- + +## 2. 원인 분석 + +### 2-1. react-rnd의 위치 제어 방식 + +`react-rnd`는 노드의 위치를 **CSS `transform: translate(x, y)`** 로 제어한다. +`position` prop에 `{ x, y }` 값을 전달하면, 내부적으로 해당 값을 `transform`에 반영한다. + +### 2-2. Stack Item의 특수 처리 (`!transform-none`) + +Stack 내부 아이템(`position: relative`)은 Flexbox 정렬을 유지해야 하므로, +`isTransformActive`가 `false`일 때 `!transform-none` CSS 클래스를 강제 적용해 +react-rnd의 transform을 무력화하고 있었다. + +```tsx +className={clsx( + "group cursor-pointer", + hasRelativePosition && !isTransformActive && "!transform-none", +)} +``` + +드래그 시작(`onDragStart`) 시에는 `setIsTransformActive(true)`로 이 클래스가 제거되고, +react-rnd의 transform이 다시 활성화된다. + +### 2-3. 버그의 핵심: 리사이즈 중 내부 position 누적 + +**`!transform-none`이 적용되어 DOM에서는 transform이 보이지 않더라도, +react-rnd 라이브러리는 리사이즈 중에 내부 position 상태를 계속 누적 변경하고 있다.** + +특히 **top 방향 / left 방향 핸들**로 리사이즈할 경우, 요소가 시각적으로 이동해야 하므로 +react-rnd가 내부적으로 `translate(x, y)` 값을 계산해 내부 ref에 저장한다. + +``` +리사이즈 전: 내부 position = { x: 0, y: 0 } +↓ top-left 방향으로 리사이즈 +리사이즈 후: 내부 position = { x: -50, y: -30 } ← 누적됨! + (DOM에서는 !transform-none으로 가려져 있어서 보이지 않음) +``` + +### 2-4. 드래그 시작 시 점프 발생 흐름 + +``` +1. onDragStart 실행 +2. setIsTransformActive(true) → !transform-none 클래스 제거 +3. react-rnd가 리사이즈 중 누적된 내부 position { x: -50, y: -30 }을 + transform에 그대로 반영 → 노드 점프! 💥 +4. d.node.offsetLeft / offsetTop 읽음 + → 이미 튀어버린 위치 기준으로 좌표를 읽음 → 잘못된 좌표로 이동 +``` + +--- + +## 3. 시도한 해결책과 왜 실패했는가 + +### ❌ 시도 1: `onResizeStop`에서 position 초기화 + +```tsx +// onResizeStop 내부 +if (hasRelativePosition) { + setDragPosition({ x: 0, y: 0 }); + rndRef.current?.updatePosition({ x: 0, y: 0 }); +} +``` + +**실패 원인:** +`onResizeStop`에서 초기화해도, 이후 사용자가 리사이즈 핸들을 다시 조작하면 +또다시 내부 position이 누적된다. +결정적으로, `onResizeStop` 이후와 `onDragStart` 사이에 사용자 인터랙션이 없어도 +react-rnd 내부에서 추가적인 position 변화가 일어날 수 있어 근본적인 해결이 어렵다. +즉, **"리사이즈가 끝날 때" 초기화는 타이밍이 맞지 않는다.** + +--- + +## 4. 최종 해결책 + +### ✅ `onDragStart`에서 항상 먼저 초기화 + +드래그가 시작되는 바로 그 시점, **transform이 활성화되기 직전**에 +`rndRef.current?.updatePosition({ x: 0, y: 0 })`을 호출해 +react-rnd 내부 position을 강제로 `{x:0, y:0}`으로 초기화한다. + +```tsx +onDragStart={(e, d) => { + e.stopPropagation(); + setDraggingId(id); + + if (hasRelativePosition) { + // ✅ 핵심: isTransformActive를 true로 바꾸기 전에 + // react-rnd 내부 position을 반드시 먼저 리셋해야 한다. + // 리사이즈 중 누적된 내부 position이 드래그 활성화와 동시에 + // transform에 반영되어 노드가 튀는 버그를 방지. + rndRef.current?.updatePosition({ x: 0, y: 0 }); + + const { offsetLeft, offsetTop } = d.node; + setIsTransformActive(true); + // ... + } +}} +``` + +### 왜 이 방법이 동작하는가? + +| 단계 | 이전(버그) | 이후(수정) | +|---|---|---| +| 리사이즈 중 | 내부 position 누적 | 내부 position 누적 (동일) | +| `onDragStart` 진입 | 누적된 값 그대로 유지 | `updatePosition(0,0)` 으로 강제 리셋 | +| `setIsTransformActive(true)` | 누적된 position이 transform에 반영 → 점프 | 내부 position이 `{0,0}`이므로 안전하게 transform 활성화 | +| `offsetLeft/offsetTop` 읽기 | 잘못된 위치 기준 | 올바른 flexbox 위치 기준 | + +**타이밍이 핵심이다.** `setIsTransformActive(true)`로 transform을 활성화하기 **이전에** +내부 position을 초기화해야 React의 렌더링 사이클에서 올바른 순서가 보장된다. + +--- + +## 5. 핵심 교훈 + +### "React 상태 초기화"와 "라이브러리 내부 상태 초기화"는 다르다 + +``` +setDragPosition({ x: 0, y: 0 }) +→ React가 관리하는 state만 초기화. react-rnd 내부 ref에는 영향 없음. + +rndRef.current?.updatePosition({ x: 0, y: 0 }) +→ react-rnd가 내부적으로 관리하는 DOM transform 상태까지 직접 초기화. +``` + +react-rnd처럼 **uncontrolled 방식으로 DOM을 직접 조작하는 라이브러리**는 +React의 state/props와 별도로 자체적인 내부 상태를 가진다. +Zustand나 useState로 아무리 상태를 바꿔도, **라이브러리 내부 상태는 별도로 초기화**해야 한다. + +### 리사이즈 방향에 따라 position이 변한다 + +react-rnd는 `bottom-right` 방향 리사이즈만 할 때는 position이 변하지 않는다. +그러나 **`top`, `left`, `top-left`, `top-right`, `bottom-left` 방향**으로 리사이즈하면 +요소가 반대 방향으로 이동해야 하므로 내부 position을 자동으로 변경한다. +이 동작은 CSS의 `transform-origin`과 다른, react-rnd 고유의 position 계산 방식이다. + +### 초기화 타이밍은 "활성화 직전"이어야 한다 + +- `onResizeStop`: 너무 이름 — 이후 또 다른 조작이 발생할 수 있음 +- `onDragStart` (transform 활성화 직전): ✅ 가장 안전한 타이밍 + +--- + +## 6. 관련 코드 위치 + +| 항목 | 위치 | +|---|---| +| 수정 파일 | `packages/ui/src/core/EditorNodeWrapper.tsx` | +| 수정 함수 | `onDragStart` 핸들러 내부 | +| 핵심 API | `rndRef.current?.updatePosition({ x: 0, y: 0 })` | +| react-rnd 타입 정의 | `node_modules/react-rnd/lib/index.d.ts` | + +--- + +## 7. 최종 적용 diff + +```diff +onDragStart={(e, d) => { + e.stopPropagation(); + setDraggingId(id); + if (hasRelativePosition) { ++ rndRef.current?.updatePosition({ x: 0, y: 0 }); // 내부 position 강제 리셋 + const { offsetLeft, offsetTop } = d.node; + setIsTransformActive(true); +- flushSync(() => { +- updateNode(id, { x: offsetLeft, y: offsetTop }); +- setDragPosition({ x: offsetLeft, y: offsetTop }); +- }); + } +}} +``` diff --git a/packages/ui/src/core/EditorNodeWrapper.tsx b/packages/ui/src/core/EditorNodeWrapper.tsx index e52114d..54ccdd6 100644 --- a/packages/ui/src/core/EditorNodeWrapper.tsx +++ b/packages/ui/src/core/EditorNodeWrapper.tsx @@ -123,16 +123,18 @@ export default function EditorNodeWrapper({ setDraggingId(id); //드래그 시작 알림 console.log(d.node.offsetLeft); if (hasRelativePosition) { + rndRef.current?.updatePosition({ x: 0, y: 0 }); + const { offsetLeft, offsetTop } = d.node; setIsTransformActive(true); console.log( `[dragStart]_현재 추출된 노드 좌표 offsetLeft - ${offsetLeft} // offsetTop - ${offsetTop} `, ); - flushSync(() => { - updateNode(id, { x: offsetLeft, y: offsetTop }); - setDragPosition({ x: offsetLeft, y: offsetTop }); - }); + // flushSync(() => { + // updateNode(id, { x: offsetLeft, y: offsetTop }); + // setDragPosition({ x: offsetLeft, y: offsetTop }); + // }); } console.log( `[dragStart]현재 노드의 실제 렌더링position - x:${x}, y:${y}`, @@ -192,10 +194,10 @@ export default function EditorNodeWrapper({ // 내부 position을 누적 변경한다. !transform-none으로 DOM에선 보이지 않지만 // 드래그 시작 시 isTransformActive가 true로 바뀌면서 누적된 값이 한꺼번에 반영되어 // 노드가 튀는 버그가 발생한다. → 리사이즈 종료 시 내부 position을 {x:0, y:0}으로 강제 동기화. - if (hasRelativePosition) { - setDragPosition({ x: 0, y: 0 }); - rndRef.current?.updatePosition({ x: 0, y: 0 }); - } + // if (hasRelativePosition) { + // setDragPosition({ x: 0, y: 0 }); + // rndRef.current?.updatePosition({ x: 0, y: 0 }); + // } }} enableResizing={isGroup ? undefined : isSelected ? undefined : false} disableDragging={!isSelected} From c3cd74031fc7338210da24f342569f7fd718c5a4 Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 25 Feb 2026 16:54:58 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix(ui):=20Stack=20item=20=EB=A6=AC?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=EC=A6=88=20=ED=9B=84=20=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EA=B7=B8=20=EC=8B=9C=20=EB=85=B8=EB=93=9C=20=EC=A0=90=ED=94=84?= =?UTF-8?q?=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [문제 상황] - Stack 내부의 `position: relative` 아이템 리사이즈 후 드래그 시작 시 노드가 엉뚱한 위치로 순간이동(Jump)하는 버그 발생 [원인 분석] - react-rnd는 내부적으로 위치 값을 계속 누적하여 `transform: translate(x, y)`으로 사용함. - Stack 아이템의 경우 Flex 정렬 유지를 위해 평소 `!transform-none` CSS 속성으로 덮어두지만, 내부적으로는 리사이즈(top/left 방향 핸들) 시 position이 계속 누적됨. - 드래그를 시작할 때(`onDragStart`) `setIsTransformActive(true)`를 통해 `!transform-none`이 제거되는 순간, 누적되어 있던 내부 position 값이 한꺼번에 적용되며 튀어버리는 현상. [해결 방법] - `onDragStart`에서 `setIsTransformActive(true)`로 transform 속성을 활성화하기 직전에 `rndRef.current?.updatePosition({ x: 0, y: 0 })`를 호출하도록 수정. - React의 상태(`dragPosition`) 업데이트만으로 라이브러리 내부 DOM 조작 상태가 완전히 초기화되지 않는 문제를 해결하며, transform 활성화 이전에 내부 상태를 우선 초기화하여 항상 깨끗한 상태로 드래그를 시작할 수 있게 함. --- packages/ui/src/core/EditorNodeWrapper.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/ui/src/core/EditorNodeWrapper.tsx b/packages/ui/src/core/EditorNodeWrapper.tsx index 54ccdd6..5945b2b 100644 --- a/packages/ui/src/core/EditorNodeWrapper.tsx +++ b/packages/ui/src/core/EditorNodeWrapper.tsx @@ -123,18 +123,13 @@ export default function EditorNodeWrapper({ setDraggingId(id); //드래그 시작 알림 console.log(d.node.offsetLeft); if (hasRelativePosition) { - rndRef.current?.updatePosition({ x: 0, y: 0 }); - const { offsetLeft, offsetTop } = d.node; + rndRef.current?.updatePosition({ x: 0, y: 0 }); setIsTransformActive(true); console.log( `[dragStart]_현재 추출된 노드 좌표 offsetLeft - ${offsetLeft} // offsetTop - ${offsetTop} `, ); - // flushSync(() => { - // updateNode(id, { x: offsetLeft, y: offsetTop }); - // setDragPosition({ x: offsetLeft, y: offsetTop }); - // }); } console.log( `[dragStart]현재 노드의 실제 렌더링position - x:${x}, y:${y}`, From 6ca0a9b84c09dae0c959215d68a71d71ba2a7c82 Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 25 Feb 2026 17:17:29 +0900 Subject: [PATCH 5/5] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EB=8B=A8=EC=88=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20-=20db=EC=9D=98=20?= =?UTF-8?q?=EB=85=B8=EB=93=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20portal=EC=9D=84=20=ED=86=B5=ED=95=B4=20?= =?UTF-8?q?=EB=85=B8=EB=93=9C=EC=9D=98=20=EB=A6=AC=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=EC=A7=95=20=ED=95=B8=EB=93=A4=EB=9F=AC=EB=A5=BC=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=A7=81=ED=95=A8=EC=97=90=20=EB=94=B0=EB=9D=BC=20?= =?UTF-8?q?=EC=8A=A4=ED=83=9D=20=EB=85=B8=EB=93=9C=EA=B0=80=20=EC=9E=90?= =?UTF-8?q?=EC=B2=B4=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EA=B0=99=EA=B3=A0=20?= =?UTF-8?q?=EC=9E=88=EB=8D=98=20=ED=95=B8=EB=93=A4=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/editor/db.json | 10 ++++------ packages/ui/src/components/Stack.tsx | 1 - 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/editor/db.json b/apps/editor/db.json index efb6540..d336570 100644 --- a/apps/editor/db.json +++ b/apps/editor/db.json @@ -15,7 +15,6 @@ "justifyContent": "center", "gap": "20px", "padding": "50px 24px", - "maxWidth": "1200px", "backgroundColor": "#FFFFFF", "className": "content-section" }, @@ -28,8 +27,8 @@ "type": "Heading", "position": 0, "layout": { - "x": 0, - "y": 0, + "x": 1000, + "y": 1000, "width": 1200, "height": 100, "zIndex": 2 @@ -94,7 +93,6 @@ "justifyContent": "center", "gap": "20px", "padding": "50px 24px", - "maxWidth": "1200px", "backgroundColor": "#F9FAFB", "className": "gallery-section" }, @@ -133,8 +131,8 @@ }, "style": { "position": "absolute", - "top": "50px", - "left": "700px", + "top": "0px", + "left": "0px", "width": "600px", "height": "300px", "objectFit": "cover", diff --git a/packages/ui/src/components/Stack.tsx b/packages/ui/src/components/Stack.tsx index d38c418..9bd518a 100644 --- a/packages/ui/src/components/Stack.tsx +++ b/packages/ui/src/components/Stack.tsx @@ -18,7 +18,6 @@ export default function StackComponent({ style={cssProps} className={cn("h-full w-full", { "node.style.className": node.style.className, - "ring-semantic-info ring-2": hoveredStackId === node.id, })} > {children}