From 875b46ff87033482250fb12dd5443e1f307aa18d Mon Sep 17 00:00:00 2001 From: y-minion Date: Tue, 10 Feb 2026 18:31:31 +0900 Subject: [PATCH 01/19] =?UTF-8?q?=E2=9C=A8=20=EC=8A=A4=ED=86=A0=EC=96=B4?= =?UTF-8?q?=20=EA=B0=9D=EC=B2=B4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit context에 스토어 객체를 주입하기 위해 createStore를 사용해 스토어 객체를 생성. 해당 스토어는 context를 통해 @repo/ui에서 사용됩니다. --- apps/editor/src/stores/useDragStore.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 apps/editor/src/stores/useDragStore.ts diff --git a/apps/editor/src/stores/useDragStore.ts b/apps/editor/src/stores/useDragStore.ts new file mode 100644 index 0000000..3bb4b6b --- /dev/null +++ b/apps/editor/src/stores/useDragStore.ts @@ -0,0 +1,23 @@ +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, get) => ({ + setDraggingId: (id: string | null) => + set((store) => (store.draggingNodeId = id)), + setHoveredStackId: (id: string | null) => + set((store) => (store.hoveredStackId = id)), + }), + ), + ), + ), +); From 901f85c73cc716dbcf5b3d11601312048c7d3385 Mon Sep 17 00:00:00 2001 From: y-minion Date: Tue, 10 Feb 2026 18:33:38 +0900 Subject: [PATCH 02/19] =?UTF-8?q?=E2=9C=A8=20drag=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EC=96=B4=20=EC=A3=BC=EC=9E=85=ED=95=98=EB=8A=94=20context?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 스토어 객체를 주입받아 하위 컴포넌트에게 전달하는 context생성. 이때 useStore를 사용해 context를 사용하는 하위 컴포넌트가 스토어의 값을 구독할 수 있도록 해줍니다. --- packages/ui/src/context/dragContext.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 packages/ui/src/context/dragContext.tsx 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); +} From 74055805dc2351af79c53fbcea91bcf484dc0fcf Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 11 Feb 2026 11:14:16 +0900 Subject: [PATCH 03/19] =?UTF-8?q?=F0=9F=92=AC=20=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EC=96=B4=20=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/editor/src/stores/useEditorStore.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/editor/src/stores/useEditorStore.ts b/apps/editor/src/stores/useEditorStore.ts index 848d58c..0aa577a 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]; @@ -262,3 +263,6 @@ export const useSetCanvas = () => useEditorStore((store) => store.setCanvas); */ export const useGetDescendantIds = () => useEditorStore((store) => store.getDescendantIds); + +export const useAddItemToStack = () => + useEditorStore((store) => store.addItemToStack); From def5944dd85e7d16fab70875980bb3fc4de2810b Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 11 Feb 2026 11:15:40 +0900 Subject: [PATCH 04/19] =?UTF-8?q?=F0=9F=90=9B=20=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EC=96=B4=EC=9D=98=20immer=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit immer를 사용함으로써 새로운 값을 반환하지 말고 기존의 상태값을 수정하도록 로직이 실행되야하는데, 새로운 값을 반환해서 버그 발생. 기존의 상태을 수정하도록 수정 완료 --- apps/editor/src/stores/useDragStore.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/editor/src/stores/useDragStore.ts b/apps/editor/src/stores/useDragStore.ts index 3bb4b6b..c87094d 100644 --- a/apps/editor/src/stores/useDragStore.ts +++ b/apps/editor/src/stores/useDragStore.ts @@ -13,9 +13,13 @@ export const useDragStore = createStore( }, (set, get) => ({ setDraggingId: (id: string | null) => - set((store) => (store.draggingNodeId = id)), + set((store) => { + store.draggingNodeId = id; + }), setHoveredStackId: (id: string | null) => - set((store) => (store.hoveredStackId = id)), + set((store) => { + store.hoveredStackId = id; + }), }), ), ), From fda2fdf214b5292cd0756d09e3535129f6dfa707 Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 11 Feb 2026 11:22:35 +0900 Subject: [PATCH 05/19] =?UTF-8?q?=E2=9C=A8=20=EB=93=9C=EB=9E=98=EA=B7=B8?= =?UTF-8?q?=20=EC=A0=84=EC=9A=A9=20=EC=8A=A4=ED=86=A0=EC=96=B4=EB=A5=BC=20?= =?UTF-8?q?=EC=BB=A8=ED=85=8D=EC=8A=A4=ED=8A=B8=EC=97=90=20=EC=A3=BC?= =?UTF-8?q?=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 모노레포 구조에서 단방향을 지키기위해 apps/editor의 드래그 스토어를 context에 주입해 @repo/ui의 컴포넌트(NodeEditorWrapper)가 사용할 수 있도록 구현 --- apps/editor/src/components/editor/Canvas.tsx | 12 +++- packages/ui/src/core/EditorNodeWrapper.tsx | 75 ++++++++++++++++++-- 2 files changed, 80 insertions(+), 7 deletions(-) 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/packages/ui/src/core/EditorNodeWrapper.tsx b/packages/ui/src/core/EditorNodeWrapper.tsx index ac27061..8de8371 100644 --- a/packages/ui/src/core/EditorNodeWrapper.tsx +++ b/packages/ui/src/core/EditorNodeWrapper.tsx @@ -1,5 +1,6 @@ //에디터 모드전용 노드 렌더러 래퍼 컴포넌트 import clsx from "clsx"; +import { useDragStore } from "context/dragContext"; import { Rnd } from "react-rnd"; import { WcxNode } from "types"; import { CanvasState, Layer } from "types/rnd"; @@ -12,6 +13,7 @@ interface WrapperProps { updateNode: (id: string, updates: Partial) => void; //노드의 레이아웃 업데이트 함수 from editor의 스토어 액션 selectNode: (id: string) => void; canvas: CanvasState; + addItemToStack: (draggedId: string, stackId: string) => void; } //에디터 전용 노드 렌더러 래퍼 @@ -24,6 +26,7 @@ export default function EditorNodeWrapper({ updateNode, selectNode, canvas, + addItemToStack, }: WrapperProps) { const isStackItem = parentNode?.type === "Stack"; const hasRelativePosition = @@ -44,6 +47,42 @@ 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 객체 반환) + function findRootStackId(e: any) { + const element = document.elementFromPoint(e.clientX, e.clientY); + if (!element) return null; + + //현재 클릭된 지점의 제일 앞에(z-index기준)있는 노드중에 stack노드 찾기 + let curStackNode = element.closest( + '[data-component-type="Stack"]', + ) as HTMLElement | null; + if (!curStackNode) return null; + + if (curStackNode.getAttribute("data-component-id") === id) { + return null; + } + + while (curStackNode) { + const parent: HTMLElement | undefined | null = + curStackNode.parentElement?.closest('[data-component-type="Stack"]'); + if (!parent) break; + curStackNode = parent; + } + return curStackNode.getAttribute("data-component-id"); + } + //TODO- 노드 선택 로직 구현, 선택 ID 공유하는 zustand 스토어 구현 필요 return ( @@ -51,15 +90,32 @@ export default function EditorNodeWrapper({ size={{ width, height }} position={{ x, y }} scale={canvas.scale} - onDragStart={(e) => e.stopPropagation()} - //TODO-일단 이동중에 스토어 업데이트는 미루기 -> 성능 이슈 - // onDrag={(e, d) => updateNode(id, { x: d.x, y: d.y })} + onDragStart={(e) => { + e.stopPropagation(); + setDraggingId(id); //드래그 시작 알림 + }} + //TODO-이동중에 로직 실행하면 성능상 부담이 될 수 있다... 최적화 고민 해보기 + onDrag={(e, d) => { + const stackId = findRootStackId(e); + if (stackId !== hoveredStackId) { + setHoveredStackId(stackId); + } + }} onDragStop={(e, d) => { + const stackId = findRootStackId(e); + + setDraggingId(null); + setHoveredStackId(null); + if (isSwitchItems) { //현재 놓인 Y위치에 따라서 노드의 순서 변경을 고려해야한다. //TODO-Stack내부에서 Item 노드의 순서 변경 로직 실행 + } else if (stackId) { + addItemToStack(id, stackId); + //addItemToStack 로직 실행 + } else { + updateNode(id, { x: d.x, y: d.y }); } - updateNode(id, { x: d.x, y: d.y }); }} onResizeStart={(e) => e.stopPropagation()} //TODO-일단 리사이징중에 스토어 업데이트는 미루기 -> 성능 이슈 @@ -81,7 +137,10 @@ export default function EditorNodeWrapper({ } enableResizing={isGroup ? undefined : isSelected ? undefined : false} disableDragging={!isSelected} - className={clsx("group cursor-pointer", isSelected && "z-50")} + className={clsx( + "group cursor-pointer", + // Allow pointer events to pass through during drag so elementFromPoint works for underlying stack + )} resizeHandleClasses={{ bottomLeft: isSelected ? clsx(selectedNodeGuideClasses.handle, "!-left-1 !-bottom-1") @@ -104,10 +163,14 @@ 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, )} > + {showGuide && ( +
+ )} + {/* 실제 컴포넌트(Hero 등)는 이 안에 렌더링됨 */} {children}
From 93a5009ee036775a2d5ba51411b78dcc2c1022e2 Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 11 Feb 2026 14:39:45 +0900 Subject: [PATCH 06/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EA=B8=B0=EC=A1=B4?= =?UTF-8?q?=20=EB=AA=A9=EB=8D=B0=EC=9D=B4=ED=84=B0=EC=9D=98=20=EB=85=B8?= =?UTF-8?q?=EB=93=9C=20=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Container 노드 타입을 STACK으로 변경 --- apps/editor/db.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/editor/db.json b/apps/editor/db.json index 307b5c4..e3f224c 100644 --- a/apps/editor/db.json +++ b/apps/editor/db.json @@ -4,7 +4,7 @@ "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": {}, @@ -71,7 +71,7 @@ "id": "1005", "page_id": 201, "parent_id": null, - "type": "Container", + "type": "Stack", "position": 2, "layout": { "x": 0, From 7026dd7b010d021be1514af20d559c4b63e25a91 Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 11 Feb 2026 14:41:50 +0900 Subject: [PATCH 07/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Stack=EB=85=B8?= =?UTF-8?q?=EB=93=9C=20=EA=B0=80=EC=9D=B4=EB=93=9C=EB=9D=BC=EC=9D=B8=20?= =?UTF-8?q?=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 컨텍스트로 주입된 호버된 스택 id를 구독하여 해당하는 스택 노드가 호버될 경우에만 가이드라인 렌더링 되도록 수정 --- packages/ui/src/components/Stack.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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} From 6d4bc8a505f36029521beaf59f3843d525b48acf Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 11 Feb 2026 14:44:17 +0900 Subject: [PATCH 08/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Stack=20=EC=B6=94?= =?UTF-8?q?=EC=A0=81=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존에 호버된 스택 노드가 중첩될 경우 언제나 root 스택노드를 찾도록 했었음. 하지만 마우스와 가장 가까운 스택 노드를 찾도록 수정 완료. 실제 UX고려하여 사용자의 마우스와 가장 가까운 스택 노드에 귀속되도록 수정 완료. --- packages/ui/src/core/EditorNodeWrapper.tsx | 42 +++++++++------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/packages/ui/src/core/EditorNodeWrapper.tsx b/packages/ui/src/core/EditorNodeWrapper.tsx index 8de8371..d134bcf 100644 --- a/packages/ui/src/core/EditorNodeWrapper.tsx +++ b/packages/ui/src/core/EditorNodeWrapper.tsx @@ -60,27 +60,26 @@ export default function EditorNodeWrapper({ //클릭된 좌표 기준 stack찾는 함수_재귀를 이용해 최상위의 Stack의 id를 반환합니다. //TODO-노드 객체만 전달해도 되는거아닌가? -> 일단 노드의 id 반환으로 처리완료.(id vs 객체 반환) + //클릭된 좌표 기준 stack찾는 함수_재귀를 이용해 최상위의 Stack의 id를 반환합니다. function findRootStackId(e: any) { - const element = document.elementFromPoint(e.clientX, e.clientY); - if (!element) return null; + const elements = document.elementsFromPoint(e.clientX, e.clientY); - //현재 클릭된 지점의 제일 앞에(z-index기준)있는 노드중에 stack노드 찾기 - let curStackNode = element.closest( - '[data-component-type="Stack"]', - ) as HTMLElement | null; - if (!curStackNode) return null; + for (const element of elements) { + let curStackNode = element.closest( + '[data-component-type="Stack"]', + ) as HTMLElement | null; - if (curStackNode.getAttribute("data-component-id") === id) { - return null; - } + if (!curStackNode) continue; + + // 드래그 중인 노드(=자기 자신)이거나 그 자손인 경우 건너뜁니다 + if (curStackNode.closest(`[data-component-id="${id}"]`)) { + continue; + } - while (curStackNode) { - const parent: HTMLElement | undefined | null = - curStackNode.parentElement?.closest('[data-component-type="Stack"]'); - if (!parent) break; - curStackNode = parent; + return curStackNode.getAttribute("data-component-id"); } - return curStackNode.getAttribute("data-component-id"); + + return null; } //TODO- 노드 선택 로직 구현, 선택 ID 공유하는 zustand 스토어 구현 필요 @@ -97,6 +96,7 @@ export default function EditorNodeWrapper({ //TODO-이동중에 로직 실행하면 성능상 부담이 될 수 있다... 최적화 고민 해보기 onDrag={(e, d) => { const stackId = findRootStackId(e); + if (stackId !== hoveredStackId) { setHoveredStackId(stackId); } @@ -137,10 +137,7 @@ export default function EditorNodeWrapper({ } enableResizing={isGroup ? undefined : isSelected ? undefined : false} disableDragging={!isSelected} - className={clsx( - "group cursor-pointer", - // Allow pointer events to pass through during drag so elementFromPoint works for underlying stack - )} + className={clsx("group cursor-pointer")} resizeHandleClasses={{ bottomLeft: isSelected ? clsx(selectedNodeGuideClasses.handle, "!-left-1 !-bottom-1") @@ -167,11 +164,6 @@ export default function EditorNodeWrapper({ isSelected && selectedNodeGuideClasses.outline, )} > - {showGuide && ( -
- )} - - {/* 실제 컴포넌트(Hero 등)는 이 안에 렌더링됨 */} {children}
From 0bf4edd4e8899eb7e3a21b5d5a75b77bd12aa1ab Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 11 Feb 2026 14:44:50 +0900 Subject: [PATCH 09/19] =?UTF-8?q?=EC=8A=A4=ED=86=A0=EC=96=B4=20=EB=94=94?= =?UTF-8?q?=EB=B2=84=EA=B9=85=20=EC=9C=84=ED=95=9C=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/editor/src/stores/useDragStore.ts | 3 +++ pnpm-lock.yaml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/apps/editor/src/stores/useDragStore.ts b/apps/editor/src/stores/useDragStore.ts index c87094d..2e1eb1c 100644 --- a/apps/editor/src/stores/useDragStore.ts +++ b/apps/editor/src/stores/useDragStore.ts @@ -23,5 +23,8 @@ export const useDragStore = createStore( }), ), ), + { + name: "dragStore", + }, ), ); 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 From 98939f6ec36bed7d322a62045cf1d1dce1d2fb39 Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 11 Feb 2026 17:09:50 +0900 Subject: [PATCH 10/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EC=8A=A4=ED=83=9D?= =?UTF-8?q?=EB=85=B8=EB=93=9C=20=EA=B5=AC=ED=98=84=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EC=9D=98=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=86=8D=EC=84=B1=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/editor/db.json | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/apps/editor/db.json b/apps/editor/db.json index e3f224c..673f600 100644 --- a/apps/editor/db.json +++ b/apps/editor/db.json @@ -9,7 +9,11 @@ "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", @@ -35,6 +39,7 @@ "level": "h2" }, "style": { + "position": "relative", "color": "#111827", "fontSize": "36px", "textAlign": "center", @@ -60,6 +65,7 @@ "level": "p" }, "style": { + "position": "relative", "color": "#374151", "fontSize": "18px", "lineHeight": 1.6, @@ -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", @@ -104,6 +112,7 @@ "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" }, @@ -142,6 +155,7 @@ "level": "p" }, "style": { + "position": "relative", "textAlign": "center", "fontSize": "14px", "color": "#6B7280", @@ -161,6 +175,7 @@ "level": "p" }, "style": { + "position": "relative", "textAlign": "center", "fontSize": "14px", "color": "#6B7280", From ceb81f36bca7fdcccd18db9b7c731213aaee5112 Mon Sep 17 00:00:00 2001 From: y-minion Date: Thu, 12 Feb 2026 12:41:36 +0900 Subject: [PATCH 11/19] =?UTF-8?q?=F0=9F=90=9B=20=EC=8A=A4=ED=83=9D=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=EC=9D=98=20Postion:=20absolute=EC=9D=B8=20it?= =?UTF-8?q?em=20=EB=85=B8=EB=93=9C=EC=9D=98=20=EC=9D=B4=EB=8F=99=EC=9D=B4?= =?UTF-8?q?=20=EC=A0=9C=ED=95=9C=EB=90=98=EB=8D=98=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 처음에는 rnd컴포넌트의 onDragStop핸들러에서 드래그가 종료된 마우스의 지점 기준으로 가장 가까이에 있는 스택 id가 존재하면 즉시 addItemToStack 로직이 실행되며 강제로 포지션의 속성을 relative로 변경하는 로직이 있었음. 하지만 해당 로직의 적용 범위는 *스택 외부의 노드*에 대해서만 적용이 되야 했는데, 스택 하위의 노드에도 적용이 되버려 강제로 position이 relative로 변경되며 이동이 제한되고 있었음. 분기처리 조건은 정확히 외부의 아이템이 스택 내부로 들어오는 경우에만 적용 되도록 수정 완료. --- apps/editor/src/stores/useEditorStore.ts | 46 ++++++++++++---------- packages/ui/src/core/EditorNodeWrapper.tsx | 19 ++++++--- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/apps/editor/src/stores/useEditorStore.ts b/apps/editor/src/stores/useEditorStore.ts index 0aa577a..b7cc2fe 100644 --- a/apps/editor/src/stores/useEditorStore.ts +++ b/apps/editor/src/stores/useEditorStore.ts @@ -151,27 +151,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", + ), }), ), ), diff --git a/packages/ui/src/core/EditorNodeWrapper.tsx b/packages/ui/src/core/EditorNodeWrapper.tsx index d134bcf..74f63f4 100644 --- a/packages/ui/src/core/EditorNodeWrapper.tsx +++ b/packages/ui/src/core/EditorNodeWrapper.tsx @@ -29,8 +29,7 @@ export default function EditorNodeWrapper({ 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; @@ -61,7 +60,7 @@ export default function EditorNodeWrapper({ //클릭된 좌표 기준 stack찾는 함수_재귀를 이용해 최상위의 Stack의 id를 반환합니다. //TODO-노드 객체만 전달해도 되는거아닌가? -> 일단 노드의 id 반환으로 처리완료.(id vs 객체 반환) //클릭된 좌표 기준 stack찾는 함수_재귀를 이용해 최상위의 Stack의 id를 반환합니다. - function findRootStackId(e: any) { + function findStackId(e: any) { const elements = document.elementsFromPoint(e.clientX, e.clientY); for (const element of elements) { @@ -95,25 +94,33 @@ export default function EditorNodeWrapper({ }} //TODO-이동중에 로직 실행하면 성능상 부담이 될 수 있다... 최적화 고민 해보기 onDrag={(e, d) => { - const stackId = findRootStackId(e); + const stackId = findStackId(e); if (stackId !== hoveredStackId) { setHoveredStackId(stackId); } }} onDragStop={(e, d) => { - const stackId = findRootStackId(e); + console.log(`현재 노드 ${id}- 포지션 ${node.style.position}`); + const stackId = findStackId(e); setDraggingId(null); setHoveredStackId(null); + if (hasRelativePosition) { + console.log("relative position"); + return; + } + //드래그가 종료될 경우에 외부 아이템이 스택으로 들어오는경우,스택 내부의 아이템이 이동할 경우(포지션 앱솔루트), 내부 아이템끼리 위치 이동할 경우,기본 위치 이동을 생각해야한다. if (isSwitchItems) { //현재 놓인 Y위치에 따라서 노드의 순서 변경을 고려해야한다. //TODO-Stack내부에서 Item 노드의 순서 변경 로직 실행 - } else if (stackId) { + } else if (stackId && node.parent_id !== stackId) { + //스택 외부의 노드가 스택 안으로 새롭게 들어오는 경우에만 해당이 된다. addItemToStack(id, stackId); //addItemToStack 로직 실행 } else { + console.log("update node"); updateNode(id, { x: d.x, y: d.y }); } }} From 59fbade8e83f0551d20c6e9a3853552780346de0 Mon Sep 17 00:00:00 2001 From: y-minion Date: Thu, 12 Feb 2026 17:50:19 +0900 Subject: [PATCH 12/19] =?UTF-8?q?=F0=9F=90=9B=20=EC=8A=A4=ED=83=9D=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=EC=9D=98=20item=EB=85=B8=EB=93=9C=EB=93=A4?= =?UTF-8?q?=EC=9D=B4=20=EC=A0=95=EB=A0=AC=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8D=98=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 에디터 노드 래퍼의 rnd컴포넌트에서 내부적으로 transform을 통해 스택의 위치를 이동시키고 있었음. 강제로 css클래스를 통해 transform없이 스택노드의 정렬에 따라 정렬되도록 수정 완료 --- packages/ui/src/core/EditorNodeWrapper.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/core/EditorNodeWrapper.tsx b/packages/ui/src/core/EditorNodeWrapper.tsx index 74f63f4..c4957aa 100644 --- a/packages/ui/src/core/EditorNodeWrapper.tsx +++ b/packages/ui/src/core/EditorNodeWrapper.tsx @@ -85,6 +85,17 @@ export default function EditorNodeWrapper({ return ( Date: Fri, 13 Feb 2026 12:25:46 +0900 Subject: [PATCH 13/19] =?UTF-8?q?=F0=9F=90=9B=20=EB=85=B8=EB=93=9C?= =?UTF-8?q?=EB=93=A4=EC=9D=98=20=EB=93=9C=EB=9E=98=EA=B7=B8=20=EB=AA=A8?= =?UTF-8?q?=EC=85=98=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 - rnd컴포넌트 특성으로 인해 스택의 정렬 속성에 따라 item 노드들이 정렬되지 않고 transform되던 문제가 있었음. 포지션이 relative고 현재 드래그 중이 아닌 경우에만 transform을 비활성화 하여 정렬되도록 강제함. - 포지션이 relative인 노드인 경우 드래그를 시작할때 이상한 위치로 튀는 버그가 있었음. 스택의 정렬에 맞게 강제로 고정했던 위치가 드래그가 시작되면서 transform으로 원래의 노드데이터 좌표로 돌아가려고 해서 발생하던게 원인이였음. 드래그가 시작되는 순간에 해당 노드의 포지션이 relative인 경우에만 현재 노드의 위치를 실시간으로 스토어에 초기화 해서 이상한 위치로 튀지 않도록 수정 완료. --- apps/editor/db.json | 14 +++++++------- packages/ui/src/core/EditorNodeWrapper.tsx | 10 +++++++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/apps/editor/db.json b/apps/editor/db.json index 673f600..efb6540 100644 --- a/apps/editor/db.json +++ b/apps/editor/db.json @@ -28,8 +28,8 @@ "type": "Heading", "position": 0, "layout": { - "x": 100, - "y": 50, + "x": 0, + "y": 0, "width": 1200, "height": 100, "zIndex": 2 @@ -54,8 +54,8 @@ "type": "Text", "position": 1, "layout": { - "x": 100, - "y": 150, + "x": 0, + "y": 0, "width": 800, "height": 200, "zIndex": 2 @@ -106,7 +106,7 @@ "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" @@ -149,7 +149,7 @@ "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" @@ -169,7 +169,7 @@ "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" diff --git a/packages/ui/src/core/EditorNodeWrapper.tsx b/packages/ui/src/core/EditorNodeWrapper.tsx index c4957aa..730695d 100644 --- a/packages/ui/src/core/EditorNodeWrapper.tsx +++ b/packages/ui/src/core/EditorNodeWrapper.tsx @@ -94,14 +94,18 @@ export default function EditorNodeWrapper({ } className={clsx( "group cursor-pointer", - hasRelativePosition && !isDraggingMyself && "!transform-none", + hasRelativePosition && !isDraggingMyself && "!transform-none", // relative인 경우에는 stack의 정렬을 지키기 위해 transform을 꺼놓는다. )} size={{ width, height }} position={{ x, y }} scale={canvas.scale} - onDragStart={(e) => { + onDragStart={(e, d) => { e.stopPropagation(); setDraggingId(id); //드래그 시작 알림 + if (hasRelativePosition) { + const { offsetLeft, offsetTop } = d.node; + updateNode(id, { x: offsetLeft, y: offsetTop }); + } }} //TODO-이동중에 로직 실행하면 성능상 부담이 될 수 있다... 최적화 고민 해보기 onDrag={(e, d) => { @@ -154,7 +158,7 @@ export default function EditorNodeWrapper({ }) } enableResizing={isGroup ? undefined : isSelected ? undefined : false} - disableDragging={!isSelected || hasRelativePosition} + disableDragging={!isSelected} resizeHandleClasses={{ bottomLeft: isSelected ? clsx(selectedNodeGuideClasses.handle, "!-left-1 !-bottom-1") From 3d967c8a0548aa52a9748de1666767d5cce1e2df Mon Sep 17 00:00:00 2001 From: y-minion Date: Fri, 20 Feb 2026 18:55:21 +0900 Subject: [PATCH 14/19] docs(editor): add JSDoc to useGetNodeById and remove TODO --- apps/editor/src/stores/useEditorStore.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/editor/src/stores/useEditorStore.ts b/apps/editor/src/stores/useEditorStore.ts index b7cc2fe..fba2612 100644 --- a/apps/editor/src/stores/useEditorStore.ts +++ b/apps/editor/src/stores/useEditorStore.ts @@ -270,3 +270,16 @@ export const useGetDescendantIds = () => 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), + ); +}; + +//노드 순서 바꾸는 훅 고민하기, 트리에서도 노드의 순서 바꿀 수 있도록 고려하기. From abaacd3eb5667a0b69845beebb81ae82facbbfc9 Mon Sep 17 00:00:00 2001 From: y-minion Date: Fri, 20 Feb 2026 18:57:15 +0900 Subject: [PATCH 15/19] =?UTF-8?q?=EB=85=B8=EB=93=9C=20=ED=8A=80=EB=8A=94?= =?UTF-8?q?=20=EB=B2=84=EA=B7=B8=20=EC=B0=BE=EA=B8=B0=20=EC=9C=84=ED=95=9C?= =?UTF-8?q?=20=EB=94=94=EB=B2=84=EA=B9=85=20=EC=A4=80=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/src/core/EditorNodeWrapper.tsx | 27 ++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/core/EditorNodeWrapper.tsx b/packages/ui/src/core/EditorNodeWrapper.tsx index 730695d..4347f78 100644 --- a/packages/ui/src/core/EditorNodeWrapper.tsx +++ b/packages/ui/src/core/EditorNodeWrapper.tsx @@ -1,6 +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"; @@ -39,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 } = node.layout; const selectedNodeGuideClasses = { @@ -94,7 +104,7 @@ export default function EditorNodeWrapper({ } className={clsx( "group cursor-pointer", - hasRelativePosition && !isDraggingMyself && "!transform-none", // relative인 경우에는 stack의 정렬을 지키기 위해 transform을 꺼놓는다. + hasRelativePosition && !isTransformActive && "!transform-none", // relative인 경우에는 stack의 정렬을 지키기 위해 transform을 꺼놓는다. )} size={{ width, height }} position={{ x, y }} @@ -105,6 +115,11 @@ export default function EditorNodeWrapper({ 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-이동중에 로직 실행하면 성능상 부담이 될 수 있다... 최적화 고민 해보기 @@ -116,11 +131,17 @@ export default function EditorNodeWrapper({ } }} 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; @@ -133,9 +154,7 @@ export default function EditorNodeWrapper({ } else if (stackId && node.parent_id !== stackId) { //스택 외부의 노드가 스택 안으로 새롭게 들어오는 경우에만 해당이 된다. addItemToStack(id, stackId); - //addItemToStack 로직 실행 } else { - console.log("update node"); updateNode(id, { x: d.x, y: d.y }); } }} @@ -154,7 +173,7 @@ 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} From 16fff70494964604313534568fb203a0480f27c2 Mon Sep 17 00:00:00 2001 From: y-minion Date: Fri, 20 Feb 2026 19:21:36 +0900 Subject: [PATCH 16/19] build: update pnpm-lock.yaml to resolve CI error --- pnpm-lock.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 151ed03..63efec5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,9 +141,6 @@ 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 From 18ee2f60d3da7de3e3322aaf53c32eacb5a9241b Mon Sep 17 00:00:00 2001 From: y-minion Date: Fri, 20 Feb 2026 19:28:18 +0900 Subject: [PATCH 17/19] build(ui): add zustand to dependencies --- packages/ui/package.json | 3 ++- pnpm-lock.yaml | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) 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/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 From 441e1d5a7d36bca482b621976eeb07d68890d9b7 Mon Sep 17 00:00:00 2001 From: y-minion Date: Fri, 20 Feb 2026 19:36:30 +0900 Subject: [PATCH 18/19] chore(editor): fix ESLint warnings in stores --- apps/editor/src/entities/section/model/store.ts | 2 +- apps/editor/src/stores/useDragStore.ts | 2 +- apps/editor/src/stores/useEditorStore.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/editor/src/entities/section/model/store.ts b/apps/editor/src/entities/section/model/store.ts index b2045cb..cd03dc9 100644 --- a/apps/editor/src/entities/section/model/store.ts +++ b/apps/editor/src/entities/section/model/store.ts @@ -10,7 +10,7 @@ interface SectionState { selectSection: (id: string) => void; } -export const useSectionStore = create((set) => ({ +export const useSectionStore = create(() => ({ sections: [ { id: 's1', name: '히어로 섹션' }, { id: 's2', name: '특징 소개' }, diff --git a/apps/editor/src/stores/useDragStore.ts b/apps/editor/src/stores/useDragStore.ts index 2e1eb1c..c60004b 100644 --- a/apps/editor/src/stores/useDragStore.ts +++ b/apps/editor/src/stores/useDragStore.ts @@ -11,7 +11,7 @@ export const useDragStore = createStore( draggingNodeId: null as null | string, hoveredStackId: null as null | string, }, - (set, get) => ({ + (set) => ({ setDraggingId: (id: string | null) => set((store) => { store.draggingNodeId = id; diff --git a/apps/editor/src/stores/useEditorStore.ts b/apps/editor/src/stores/useEditorStore.ts index fba2612..fac6140 100644 --- a/apps/editor/src/stores/useEditorStore.ts +++ b/apps/editor/src/stores/useEditorStore.ts @@ -129,6 +129,7 @@ const useEditorStore = create( // 1. 최상위 속성 업데이트 (type 등) // (주의: 객체 타입인 style, props, layout을 통째로 덮어쓰지 않도록 별도 처리) + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { style, props, layout, ...rest } = updates; Object.assign(targetNode, rest); From 81123b6ecc255ab0032afd325eac2927b575f727 Mon Sep 17 00:00:00 2001 From: y-minion Date: Fri, 20 Feb 2026 19:47:14 +0900 Subject: [PATCH 19/19] fix(ui): remove duplicate style prop in EditorNodeWrapper --- packages/ui/src/core/EditorNodeWrapper.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/ui/src/core/EditorNodeWrapper.tsx b/packages/ui/src/core/EditorNodeWrapper.tsx index 46f3213..f256e78 100644 --- a/packages/ui/src/core/EditorNodeWrapper.tsx +++ b/packages/ui/src/core/EditorNodeWrapper.tsx @@ -95,13 +95,7 @@ export default function EditorNodeWrapper({ return (