diff --git a/apps/editor/db.json b/apps/editor/db.json
index 307b5c4..efb6540 100644
--- a/apps/editor/db.json
+++ b/apps/editor/db.json
@@ -4,12 +4,16 @@
"id": "1002",
"page_id": 201,
"parent_id": null,
- "type": "Container",
+ "type": "Stack",
"position": 1,
"layout": { "x": 0, "y": 0, "width": 1440, "height": 800, "zIndex": 1 },
"props": {},
"style": {
- "display": "block",
+ "display": "flex",
+ "flexDirection": "column",
+ "alignItems": "center",
+ "justifyContent": "center",
+ "gap": "20px",
"padding": "50px 24px",
"maxWidth": "1200px",
"backgroundColor": "#FFFFFF",
@@ -24,8 +28,8 @@
"type": "Heading",
"position": 0,
"layout": {
- "x": 100,
- "y": 50,
+ "x": 0,
+ "y": 0,
"width": 1200,
"height": 100,
"zIndex": 2
@@ -35,6 +39,7 @@
"level": "h2"
},
"style": {
+ "position": "relative",
"color": "#111827",
"fontSize": "36px",
"textAlign": "center",
@@ -49,8 +54,8 @@
"type": "Text",
"position": 1,
"layout": {
- "x": 100,
- "y": 150,
+ "x": 0,
+ "y": 0,
"width": 800,
"height": 200,
"zIndex": 2
@@ -60,6 +65,7 @@
"level": "p"
},
"style": {
+ "position": "relative",
"color": "#374151",
"fontSize": "18px",
"lineHeight": 1.6,
@@ -71,7 +77,7 @@
"id": "1005",
"page_id": 201,
"parent_id": null,
- "type": "Container",
+ "type": "Stack",
"position": 2,
"layout": {
"x": 0,
@@ -82,8 +88,10 @@
},
"props": { "id": "gallery" },
"style": {
- "display": "grid",
- "gridTemplateColumns": "1fr 1fr",
+ "display": "flex",
+ "flexDirection": "row",
+ "flexWrap": "wrap",
+ "justifyContent": "center",
"gap": "20px",
"padding": "50px 24px",
"maxWidth": "1200px",
@@ -98,12 +106,13 @@
"parent_id": "1005",
"type": "Image",
"position": 0,
- "layout": { "x": 50, "y": 50, "width": 600, "height": 300, "zIndex": 2 },
+ "layout": { "x": 0, "y": 0, "width": 600, "height": 300, "zIndex": 2 },
"props": {
"src": "data:image/jpeg;base64,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",
"alt": "갤러리 이미지 1"
},
"style": {
+ "position": "relative",
"width": "100%",
"height": "300px",
"objectFit": "cover",
@@ -123,10 +132,14 @@
"alt": "갤러리 이미지 2"
},
"style": {
- "width": "100%",
+ "position": "absolute",
+ "top": "50px",
+ "left": "700px",
+ "width": "600px",
"height": "300px",
"objectFit": "cover",
- "borderRadius": "8px"
+ "borderRadius": "8px",
+ "zIndex": 10
},
"created_at": "2025-11-13T06:25:00Z"
},
@@ -136,12 +149,13 @@
"parent_id": "1005",
"type": "Text",
"position": 2,
- "layout": { "x": 50, "y": 360, "width": 600, "height": 50, "zIndex": 2 },
+ "layout": { "x": 0, "y": 0, "width": 600, "height": 50, "zIndex": 2 },
"props": {
"text": "이미지 설명 1",
"level": "p"
},
"style": {
+ "position": "relative",
"textAlign": "center",
"fontSize": "14px",
"color": "#6B7280",
@@ -155,12 +169,13 @@
"parent_id": "1005",
"type": "Text",
"position": 3,
- "layout": { "x": 700, "y": 360, "width": 600, "height": 50, "zIndex": 2 },
+ "layout": { "x": 0, "y": 0, "width": 600, "height": 50, "zIndex": 2 },
"props": {
"text": "이미지 설명 2",
"level": "p"
},
"style": {
+ "position": "relative",
"textAlign": "center",
"fontSize": "14px",
"color": "#6B7280",
diff --git a/apps/editor/src/components/editor/Canvas.tsx b/apps/editor/src/components/editor/Canvas.tsx
index 75604b1..ee8fac2 100644
--- a/apps/editor/src/components/editor/Canvas.tsx
+++ b/apps/editor/src/components/editor/Canvas.tsx
@@ -1,6 +1,8 @@
"use client";
+import { useDragStore } from "@/stores/useDragStore";
import {
+ useAddItemToStack,
useCanvas,
useClearNode,
useCurNodes,
@@ -15,6 +17,7 @@ import {
handleMouseUp,
} from "@/utils/editor/canvasMouseHandler";
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 { WcxNode } from "@repo/ui/types/nodes";
@@ -28,7 +31,9 @@ export default function Canvas() {
const canvasState = useCanvas();
const setCanvas = useSetCanvas();
const clearNode = useClearNode();
+ const addItemToStack = useAddItemToStack();
+ //TODO-이거 뭐임?
const isPanning = useRef(false);
const lastMousePos = useRef({ x: 0, y: 0 });
@@ -65,6 +70,7 @@ export default function Canvas() {
updateNode={updateNode}
selectNode={selectNode}
canvas={canvasState}
+ addItemToStack={addItemToStack}
>
@@ -91,6 +97,7 @@ export default function Canvas() {
updateNode={updateNode}
selectNode={selectNode}
canvas={canvasState}
+ addItemToStack={addItemToStack}
>
{children}
@@ -123,7 +130,10 @@ export default function Canvas() {
{/* 배경 격자 (Helper Grid) */}
- {renderTree({ id: null })}
+ {/* 실제 스토어 인스턴스를 주입 */}
+
+ {renderTree({ id: null })}
+
diff --git a/apps/editor/src/stores/useDragStore.ts b/apps/editor/src/stores/useDragStore.ts
new file mode 100644
index 0000000..c60004b
--- /dev/null
+++ b/apps/editor/src/stores/useDragStore.ts
@@ -0,0 +1,30 @@
+import { createStore } from "zustand";
+import { combine, devtools } from "zustand/middleware";
+import { immer } from "zustand/middleware/immer";
+
+//context에서 스토어를 주입하기 위해 스토어 객체로 생성
+export const useDragStore = createStore(
+ devtools(
+ immer(
+ combine(
+ {
+ draggingNodeId: null as null | string,
+ hoveredStackId: null as null | string,
+ },
+ (set) => ({
+ setDraggingId: (id: string | null) =>
+ set((store) => {
+ store.draggingNodeId = id;
+ }),
+ setHoveredStackId: (id: string | null) =>
+ set((store) => {
+ store.hoveredStackId = id;
+ }),
+ }),
+ ),
+ ),
+ {
+ name: "dragStore",
+ },
+ ),
+);
diff --git a/apps/editor/src/stores/useEditorStore.ts b/apps/editor/src/stores/useEditorStore.ts
index f034a32..fac6140 100644
--- a/apps/editor/src/stores/useEditorStore.ts
+++ b/apps/editor/src/stores/useEditorStore.ts
@@ -69,6 +69,7 @@ const useEditorStore = create(
if (!targetNode) return;
const parentNodeId = targetNode.parent_id;
+ //선택하려는 노드가 최상위 노드일 경우(부모가 ROOT)
if (parentNodeId === null) {
set((state) => {
state.selectedDepthPath = [targetNodeId];
@@ -129,7 +130,7 @@ const useEditorStore = create(
// 1. 최상위 속성 업데이트 (type 등)
// (주의: 객체 타입인 style, props, layout을 통째로 덮어쓰지 않도록 별도 처리)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- const { style, props, layout: _layout, ...rest } = updates;
+ const { style, props, layout, ...rest } = updates;
Object.assign(targetNode, rest);
// 2. 하위 객체 병합 업데이트
@@ -151,27 +152,31 @@ const useEditorStore = create(
//TODO-'Node참조값 전달' vs nodeId 전달후 스코프 안에서 파싱 고민해보기
addItemToStack: (nodeId: string, stackId: string) =>
- set((state) => {
- if (!state.nodes) return state;
- const node = state.nodes.find((n) => n.id === nodeId);
- const stack = state.nodes.find((n) => n.id === stackId);
- if (!node || !stack || stack.type !== "Stack") {
- return state;
- }
-
- // Stack의 현재 items
- //Stack노드의 하위 자식들을 'position'Props에 따라 오름차순 정렬
- const currentItems = state.nodes
- .filter((n) => n.parent_id === stackId)
- .sort((a, b) => a.position - b.position);
-
- //오름차순 정렬후 마지막 idx 배정
- const insertIndex = currentItems.length;
- // insertIndex 이후의 items position 업데이트
- node.position = insertIndex;
- node.parent_id = stackId;
- node.style.position = "relative";
- }),
+ set(
+ (state) => {
+ if (!state.nodes) return state;
+ const node = state.nodes.find((n) => n.id === nodeId);
+ const stack = state.nodes.find((n) => n.id === stackId);
+ if (!node || !stack || stack.type !== "Stack") {
+ return state;
+ }
+
+ // Stack의 현재 items
+ //Stack노드의 하위 자식들을 'position'Props에 따라 오름차순 정렬
+ const currentItems = state.nodes
+ .filter((n) => n.parent_id === stackId)
+ .sort((a, b) => a.position - b.position);
+
+ //오름차순 정렬후 마지막 idx 배정
+ const insertIndex = currentItems.length;
+ // insertIndex 이후의 items position 업데이트
+ node.position = insertIndex;
+ node.parent_id = stackId;
+ node.style.position = "relative";
+ },
+ false,
+ "editStore/addItemToStack",
+ ),
}),
),
),
@@ -263,3 +268,19 @@ export const useSetCanvas = () => useEditorStore((store) => store.setCanvas);
*/
export const useGetDescendantIds = () =>
useEditorStore((store) => store.getDescendantIds);
+
+export const useAddItemToStack = () =>
+ useEditorStore((store) => store.addItemToStack);
+
+/**
+ * [Selector] ID를 기준으로 특정 노드 객체를 반환합니다.
+ * 해당 ID의 노드가 업데이트되면 이를 사용하는 컴포넌트만 리렌더링됩니다.
+ * @param nodeId - 찾고자 하는 노드의 ID
+ */
+export const useGetNodeById = (nodeId: string) => {
+ return useEditorStore((store) =>
+ store.nodes?.find(({ id }) => id === nodeId),
+ );
+};
+
+//노드 순서 바꾸는 훅 고민하기, 트리에서도 노드의 순서 바꿀 수 있도록 고려하기.
diff --git a/packages/ui/package.json b/packages/ui/package.json
index b5320e0..ffb13ca 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -26,6 +26,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.23.26",
- "react-rnd": "^10.5.2"
+ "react-rnd": "^10.5.2",
+ "zustand": "^5.0.8"
}
}
diff --git a/packages/ui/src/components/Stack.tsx b/packages/ui/src/components/Stack.tsx
index b1e0f15..d38c418 100644
--- a/packages/ui/src/components/Stack.tsx
+++ b/packages/ui/src/components/Stack.tsx
@@ -1,3 +1,5 @@
+import { cn } from "@repo/utils";
+import { useDragStore } from "context/dragContext";
import { NodeComponentProps, StackNode } from "types";
import processNodeStyles from "utils/processNodeStyles";
@@ -5,6 +7,8 @@ export default function StackComponent({
node,
children,
}: NodeComponentProps) {
+ const hoveredStackId = useDragStore((s) => s.hoveredStackId);
+
const cssProps = processNodeStyles(node.style);
return (
@@ -12,7 +16,10 @@ export default function StackComponent({
data-component-type={node.type}
data-component-id={node.id}
style={cssProps}
- className={`${node.style.className || ""} h-full w-full`}
+ className={cn("h-full w-full", {
+ "node.style.className": node.style.className,
+ "ring-semantic-info ring-2": hoveredStackId === node.id,
+ })}
>
{children}
diff --git a/packages/ui/src/context/dragContext.tsx b/packages/ui/src/context/dragContext.tsx
new file mode 100644
index 0000000..14e27d2
--- /dev/null
+++ b/packages/ui/src/context/dragContext.tsx
@@ -0,0 +1,23 @@
+import { createContext, useContext } from "react";
+import { StoreApi, useStore } from "zustand";
+
+export interface DragState {
+ draggingNodeId: string | null;
+ hoveredStackId: string | null;
+ setDraggingId: (id: string | null) => void;
+ setHoveredStackId: (id: string | null) => void;
+}
+
+//Context는 스토어 객체를 운반_스토어의 값(state)가 아니라 스토어 객체(StoreApi)를 담는 그릇 생성.
+const DragStoreContext = createContext | null>(null);
+
+export const DragProvider = DragStoreContext.Provider;
+
+//구독하려는 하위 컴포넌트에서 구독할 수 있는 훅을 사용할 수 있도록 해야한다.
+export function useDragStore(selector: (state: DragState) => T): T {
+ //스토어 객체를 먼저 가져온다.
+ const store = useContext(DragStoreContext);
+ if (!store) throw new Error("useDragStore must be used within DragProvider");
+ //가져온 스토어 객체를 사용해 하위 컴포넌트의 구독 시스템을 활성화
+ return useStore(store, selector);
+}
diff --git a/packages/ui/src/core/EditorNodeWrapper.tsx b/packages/ui/src/core/EditorNodeWrapper.tsx
index e197804..f256e78 100644
--- a/packages/ui/src/core/EditorNodeWrapper.tsx
+++ b/packages/ui/src/core/EditorNodeWrapper.tsx
@@ -1,5 +1,7 @@
//에디터 모드전용 노드 렌더러 래퍼 컴포넌트
import clsx from "clsx";
+import { useDragStore } from "context/dragContext";
+import { useRef, useState } from "react";
import { Rnd } from "react-rnd";
import { WcxNode } from "types";
import { CanvasState, Layer } from "types/rnd";
@@ -12,6 +14,7 @@ interface WrapperProps {
updateNode: (id: string, updates: Partial) => void; //노드의 레이아웃 업데이트 함수 from editor의 스토어 액션
selectNode: (id: string) => void;
canvas: CanvasState;
+ addItemToStack: (draggedId: string, stackId: string) => void;
}
//에디터 전용 노드 렌더러 래퍼
@@ -24,10 +27,10 @@ export default function EditorNodeWrapper({
updateNode,
selectNode,
canvas,
+ addItemToStack,
}: WrapperProps) {
const isStackItem = parentNode?.type === "Stack";
- const hasRelativePosition =
- !node.style.position || node.style.position === "relative";
+ const hasRelativePosition = node.style.position === "relative";
const isSwitchItems = isStackItem && hasRelativePosition;
@@ -37,6 +40,15 @@ export default function EditorNodeWrapper({
cursor: "move",
};
+ const [isTransformActive, setIsTransformActive] = useState(false);
+ const [dragPosition, setDragPosition] = useState<{
+ x: number;
+ y: number;
+ }>({
+ x: 0,
+ y: 0,
+ });
+
const { id } = node;
const { width, height, x, y, zIndex } = node.layout;
const selectedNodeGuideClasses = {
@@ -44,27 +56,106 @@ export default function EditorNodeWrapper({
outline: "ring ring-2 ring-rnd-handle",
};
+ // 필요한 데이터만 구독
+ const draggingId = useDragStore((s) => s.draggingNodeId);
+ const hoveredStackId = useDragStore((s) => s.hoveredStackId);
+ const setDraggingId = useDragStore((s) => s.setDraggingId);
+ const setHoveredStackId = useDragStore((s) => s.setHoveredStackId);
+
+ //데이터를 바탕으로 가이드 표시 여부 결정
+ const isDraggingMyself = draggingId === id;
+ const isHoveredStack = hoveredStackId === id;
+ const showGuide = isHoveredStack && draggingId && !isDraggingMyself;
+
+ //클릭된 좌표 기준 stack찾는 함수_재귀를 이용해 최상위의 Stack의 id를 반환합니다.
+ //TODO-노드 객체만 전달해도 되는거아닌가? -> 일단 노드의 id 반환으로 처리완료.(id vs 객체 반환)
+ //클릭된 좌표 기준 stack찾는 함수_재귀를 이용해 최상위의 Stack의 id를 반환합니다.
+ function findStackId(e: any) {
+ const elements = document.elementsFromPoint(e.clientX, e.clientY);
+
+ for (const element of elements) {
+ let curStackNode = element.closest(
+ '[data-component-type="Stack"]',
+ ) as HTMLElement | null;
+
+ if (!curStackNode) continue;
+
+ // 드래그 중인 노드(=자기 자신)이거나 그 자손인 경우 건너뜁니다
+ if (curStackNode.closest(`[data-component-id="${id}"]`)) {
+ continue;
+ }
+
+ return curStackNode.getAttribute("data-component-id");
+ }
+
+ return null;
+ }
+
//TODO- 노드 선택 로직 구현, 선택 ID 공유하는 zustand 스토어 구현 필요
return (
e.stopPropagation()}
- //TODO-일단 이동중에 스토어 업데이트는 미루기 -> 성능 이슈
- // onDrag={(e, d) => updateNode(id, { x: d.x, y: d.y })}
+ onDragStart={(e, d) => {
+ e.stopPropagation();
+ setDraggingId(id); //드래그 시작 알림
+ 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} `,
+ );
+ }
+ }}
+ //TODO-이동중에 로직 실행하면 성능상 부담이 될 수 있다... 최적화 고민 해보기
+ onDrag={(e, d) => {
+ const stackId = findStackId(e);
+
+ if (stackId !== hoveredStackId) {
+ setHoveredStackId(stackId);
+ }
+ }}
onDragStop={(e, d) => {
+ setIsTransformActive(false);
+ console.log(`현재 노드 ${id}- 포지션 ${node.style.position}`);
+ const stackId = findStackId(e);
+
+ setDraggingId(null);
+ setHoveredStackId(null);
+
+ console.log(
+ `드래그 종료시 노드의 좌표 x:${d.node.offsetLeft}, y:${d.node.offsetTop}`,
+ );
+
+ if (hasRelativePosition) {
+ console.log("relative position");
+ return;
+ }
+
+ //드래그가 종료될 경우에 외부 아이템이 스택으로 들어오는경우,스택 내부의 아이템이 이동할 경우(포지션 앱솔루트), 내부 아이템끼리 위치 이동할 경우,기본 위치 이동을 생각해야한다.
if (isSwitchItems) {
//현재 놓인 Y위치에 따라서 노드의 순서 변경을 고려해야한다.
//TODO-Stack내부에서 Item 노드의 순서 변경 로직 실행
+ } else if (stackId && node.parent_id !== stackId) {
+ //스택 외부의 노드가 스택 안으로 새롭게 들어오는 경우에만 해당이 된다.
+ addItemToStack(id, stackId);
+ } else {
+ updateNode(id, { x: d.x, y: d.y });
}
- updateNode(id, { x: d.x, y: d.y });
}}
onResizeStart={(e) => e.stopPropagation()}
//TODO-일단 리사이징중에 스토어 업데이트는 미루기 -> 성능 이슈
@@ -81,12 +172,11 @@ export default function EditorNodeWrapper({
updateNode(id, {
width: parseInt(ref.style.width),
height: parseInt(ref.style.height),
- ...pos,
+ ...(hasRelativePosition ? {} : pos),
})
}
enableResizing={isGroup ? undefined : isSelected ? undefined : false}
disableDragging={!isSelected}
- className={clsx("group cursor-pointer", isSelected && "z-50")}
resizeHandleClasses={{
bottomLeft: isSelected
? clsx(selectedNodeGuideClasses.handle, "!-left-1 !-bottom-1")
@@ -109,11 +199,10 @@ export default function EditorNodeWrapper({
}}
style={wrapperStyle}
className={clsx(
- "h-full w-full transition-shadow duration-200",
+ "relative h-full w-full transition-shadow duration-200",
isSelected && selectedNodeGuideClasses.outline,
)}
>
- {/* 실제 컴포넌트(Hero 등)는 이 안에 렌더링됨 */}
{children}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 63efec5..151ed03 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -141,6 +141,9 @@ importers:
react-rnd:
specifier: ^10.5.2
version: 10.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ zustand:
+ specifier: ^5.0.8
+ version: 5.0.8(@types/react@19.2.6)(immer@11.1.3)(react@19.1.0)
devDependencies:
'@tailwindcss/postcss':
specifier: ^4