From 03d5a769c72819643a33328d28b5548cb6d4d15e Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 12 Jan 2026 18:11:41 +0900 Subject: [PATCH 01/21] =?UTF-8?q?=F0=9F=93=9D=20=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EC=96=B4=20=EC=95=A1=EC=85=98=20TODO=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= 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 d18d90f..6a7536b 100644 --- a/apps/editor/src/stores/useEditorStore.ts +++ b/apps/editor/src/stores/useEditorStore.ts @@ -43,6 +43,8 @@ const useEditorStore = create( } }); }, + + //TODO-노드 상세 데이터 수정하는 액션 추가 필요 setCanvas(updates: CanvasState) { set((state) => { state.canvas = { ...state.canvas, ...updates }; @@ -71,3 +73,5 @@ export const useUpdateNode = () => useEditorStore((store) => store.updateNode); export const useCanvas = () => useEditorStore((store) => store.canvas); export const useSetCanvas = () => useEditorStore((store) => store.setCanvas); + +//TODO-매니페스트, 현재 선택된 노드의 레이아웃 상태 구독 훅 추가 필요 -> 오른쪽 사이드 바에서 실시간으로 변경되는 x,y좌표 렌더링 할때 필요 From f8eb82d5824d19fc9cb5e029283a39b9269188bf Mon Sep 17 00:00:00 2001 From: y-minion Date: Tue, 13 Jan 2026 13:06:51 +0900 Subject: [PATCH 02/21] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EB=85=B8=EB=93=9C?= =?UTF-8?q?=20=EB=A6=AC=EC=82=AC=EC=9D=B4=EC=A6=88=20=ED=95=B8=EB=93=A4=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/src/styles.css | 1 + packages/ui/src/core/EditorNodeWrapper.tsx | 14 ++++++-------- packages/ui/src/styles.css | 1 + packages/ui/src/types/rnd.ts | 4 ++-- packages/ui/theme.css | 18 ++++++++++++++++++ 5 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 packages/ui/theme.css diff --git a/apps/editor/src/styles.css b/apps/editor/src/styles.css index f1d8c73..f801e2d 100644 --- a/apps/editor/src/styles.css +++ b/apps/editor/src/styles.css @@ -1 +1,2 @@ @import "tailwindcss"; +@import "../../../packages/ui/theme.css"; diff --git a/packages/ui/src/core/EditorNodeWrapper.tsx b/packages/ui/src/core/EditorNodeWrapper.tsx index b531a48..1f18503 100644 --- a/packages/ui/src/core/EditorNodeWrapper.tsx +++ b/packages/ui/src/core/EditorNodeWrapper.tsx @@ -25,16 +25,14 @@ export default function EditorNodeWrapper({ }: WrapperProps) { const isSelected = selectedId === node.id; const wrapperStyle: React.CSSProperties = { - // 선택되었을 때 시각적 피드백 (테두리 등) - outline: isSelected ? "outline outline-gray-500 outline-2" : "none", cursor: "move", }; const { id } = node; const { width, height, x, y } = node.layout; const selectedNodeGuideClasses = { - handle: "bg-white border rounded-full border-gray-500 !w-3 !h-3", - outline: "ring ring-2 ring-gray-500", + handle: "bg-white border-2 rounded-full border-rnd-handle !w-2 !h-2 ", + outline: "ring ring-2 ring-rnd-handle", }; //TODO- 노드 선택 로직 구현, 선택 ID 공유하는 zustand 스토어 구현 필요 @@ -71,16 +69,16 @@ export default function EditorNodeWrapper({ className={clsx("group cursor-pointer", isSelected && "z-50")} resizeHandleClasses={{ bottomLeft: isSelected - ? clsx(selectedNodeGuideClasses.handle, "-left-1.5 -bottom-1.5") + ? clsx(selectedNodeGuideClasses.handle, "-left-1 -bottom-1") : undefined, bottomRight: isSelected - ? clsx(selectedNodeGuideClasses.handle, "-right-1.5 -bottom-1.5") + ? clsx(selectedNodeGuideClasses.handle, "-right-1 -bottom-1") : undefined, topLeft: isSelected - ? clsx(selectedNodeGuideClasses.handle, "-left-1.5 -top-1.5") + ? clsx(selectedNodeGuideClasses.handle, "-left-1 -top-1") : undefined, topRight: isSelected - ? clsx(selectedNodeGuideClasses.handle, "-right-1.5 -top-1.5") + ? clsx(selectedNodeGuideClasses.handle, "-right-1 -top-1") : undefined, }} > diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css index f1d8c73..89290f7 100644 --- a/packages/ui/src/styles.css +++ b/packages/ui/src/styles.css @@ -1 +1,2 @@ @import "tailwindcss"; +@import "../theme.css"; diff --git a/packages/ui/src/types/rnd.ts b/packages/ui/src/types/rnd.ts index 19c3bdc..90c2ee3 100644 --- a/packages/ui/src/types/rnd.ts +++ b/packages/ui/src/types/rnd.ts @@ -4,8 +4,8 @@ export interface Layer { y: number; width: number; height: number; - fill: string; - content?: string; + fill: string; //색상 + content?: string; //내부 글자 } export interface CanvasState { diff --git a/packages/ui/theme.css b/packages/ui/theme.css new file mode 100644 index 0000000..33f4ba9 --- /dev/null +++ b/packages/ui/theme.css @@ -0,0 +1,18 @@ +@theme { + /* Rnd 핸들 고유 색상 */ + --color-rnd-handle: #0066ff; + + /* CORE COLOR PALETTE _ 기본 색상 - 버튼, 텍스트, 테두리 등 주요 요소에 사용 */ + --color-core-zinc-50: #fafafa; + --color-core-zinc-100: #f4f4f5; + --color-core-zinc-200: #e4e4e7; + --color-core-zinc-300: #d4d4d8; + --color-core-zinc-400: #a1a1aa; + --color-core-zinc-500: #52525b; + + /* semantic Colors _ 상태 고유 색상 */ + --color-semantic-success: #10b981; + --color-semantic-warning: #f59e0b; + --color-semantic-error: #ef4444; + --color-semantic-info: #3b82f6; +} From a121d5734640dda130124c6eafe92faa018a1c65 Mon Sep 17 00:00:00 2001 From: y-minion Date: Tue, 13 Jan 2026 17:48:24 +0900 Subject: [PATCH 03/21] =?UTF-8?q?=E2=9C=A8=20=EC=82=AD=EC=A0=9C=ED=95=A0?= =?UTF-8?q?=20=EB=85=B8=EB=93=9C=20id=20=ED=83=90=EC=83=89=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 선택된 노드의 id만 추출하는게 아닌 하위의 자식 노드들까지 트리를 타고 내려가서 모든 노드 Id를 추출하는 함수 구현 --- apps/editor/src/stores/useEditorStore.ts | 40 +++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/apps/editor/src/stores/useEditorStore.ts b/apps/editor/src/stores/useEditorStore.ts index 6a7536b..70e83ba 100644 --- a/apps/editor/src/stores/useEditorStore.ts +++ b/apps/editor/src/stores/useEditorStore.ts @@ -13,7 +13,37 @@ const useEditorStore = create( nodes: null as null | WcxNode[], //TODO- 추후에 현재 페이지에 해당하는 노드들을 받아오는 로직을 통해 해당 상태가 업데이트 되야 한다. canvas: { dx: 0, dy: 0, scale: 1 }, }, - (set) => ({ + (set, get) => ({ + setNode(nodes: WcxNode[]) { + set((state) => { + state.nodes = nodes; + }); + }, + //TODO-노드를 추가/삭제 하는 기능 필요(에디터 섹션에서 노드 추가, 삭제하는 경우 ) -> addNode & deleteNode(자식 노드까지 재귀적으로 삭제 필요!) + addNode(node: WcxNode) { + set((state) => { + state.nodes?.push(node); + }); + }, + deleteNode(nodeId: string) { + set((state) => { + if (!state.nodes) return; + const deleteNodes = []; + function recursionDeleteNode(nodeId: string) { + deleteNodes.push(nodeId); + const childrenNodes = state.nodes?.filter( + ({ parent_id }) => parent_id === nodeId, + ); + + if (childrenNodes?.length === 0) return; + + childrenNodes?.forEach(({ id }) => recursionDeleteNode(id)); + } + + recursionDeleteNode(nodeId); + //TODO- 재귀 삭제 함수로 추출된 노드id는 deleteNodes에 담겨 있다. 이 데이터를 바탕으로 DB수정 시도 + }); + }, selectNode(id: string) { set( (state) => { @@ -32,8 +62,8 @@ const useEditorStore = create( "editorStore/clearNode", ); }, - //TODO-updateNode액션 검토하기 - updateNode(targetNodeId: string, updates: Partial) { + //TODO-updateNode는 현재 오직 레이아웃(node.layout)만 변경이 가능하다. -> 액션 이름 수정 필요..? + updateNodeLayout(targetNodeId: string, updates: Partial) { set((state) => { const targetNode = state.nodes?.find( ({ id }) => id === targetNodeId, @@ -59,6 +89,7 @@ const useEditorStore = create( ), ); +//TODO-각 커스텀훅 사용 설명 주석 달기 export const useSelectedNodeId = () => useEditorStore((store) => store.selectedNodeId); @@ -68,7 +99,8 @@ export const useClearNode = () => useEditorStore((store) => store.clearNode); export const useCurNodes = () => useEditorStore((store) => store.nodes); -export const useUpdateNode = () => useEditorStore((store) => store.updateNode); +export const useUpdateNodeLayoutLayoutLayout = () => + useEditorStore((store) => store.updateNodeLayout); export const useCanvas = () => useEditorStore((store) => store.canvas); From 031a549a78843010e70454da3b6e69af054c3390 Mon Sep 17 00:00:00 2001 From: y-minion Date: Tue, 13 Jan 2026 18:48:05 +0900 Subject: [PATCH 04/21] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=ED=83=80=EA=B2=9F?= =?UTF-8?q?=20=EB=85=B8=EB=93=9C=20id=EC=9D=98=20=EC=9E=90=EC=8B=9D=20?= =?UTF-8?q?=EB=85=B8=EB=93=9Cid=EC=B6=94=EC=B6=9C=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존에 set을 이용해 구현했으나, 실제로 상태는 수정하지 않고 삭제할 노드id를 리스트로 반환하는 로직이었음. 실제 상태를 수정하지 않고 set을 쓰고 있으므로 Selector 함수로 수정. 이제 삭제할 노드 Id 리스트를 자유롭게 참조할 수 있도록 수정 완료함. --- apps/editor/src/stores/useEditorStore.ts | 38 ++++++++++++++---------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/apps/editor/src/stores/useEditorStore.ts b/apps/editor/src/stores/useEditorStore.ts index 70e83ba..8d1c17d 100644 --- a/apps/editor/src/stores/useEditorStore.ts +++ b/apps/editor/src/stores/useEditorStore.ts @@ -25,24 +25,27 @@ const useEditorStore = create( state.nodes?.push(node); }); }, - deleteNode(nodeId: string) { - set((state) => { - if (!state.nodes) return; - const deleteNodes = []; - function recursionDeleteNode(nodeId: string) { - deleteNodes.push(nodeId); - const childrenNodes = state.nodes?.filter( - ({ parent_id }) => parent_id === nodeId, - ); + getDescendantIds(nodeId: string): string[] { + const nodes = get().nodes; + if (!nodes) return []; - if (childrenNodes?.length === 0) return; + const res: string[] = []; - childrenNodes?.forEach(({ id }) => recursionDeleteNode(id)); - } + function recursionNode(nodeId: string) { + res.push(nodeId); + const childrenNodes = nodes?.filter( + ({ parent_id }) => parent_id === nodeId, + ); - recursionDeleteNode(nodeId); - //TODO- 재귀 삭제 함수로 추출된 노드id는 deleteNodes에 담겨 있다. 이 데이터를 바탕으로 DB수정 시도 - }); + if (childrenNodes?.length === 0) return; + + childrenNodes?.forEach(({ id }) => recursionNode(id)); + } + + recursionNode(nodeId); + + return res; + //TODO- 재귀 삭제 함수로 추출된 노드id는 deleteNodes에 담겨 있다. 이 데이터를 바탕으로 DB수정 시도 }, selectNode(id: string) { set( @@ -99,7 +102,7 @@ export const useClearNode = () => useEditorStore((store) => store.clearNode); export const useCurNodes = () => useEditorStore((store) => store.nodes); -export const useUpdateNodeLayoutLayoutLayout = () => +export const useUpdateNodeLayout = () => useEditorStore((store) => store.updateNodeLayout); export const useCanvas = () => useEditorStore((store) => store.canvas); @@ -107,3 +110,6 @@ export const useCanvas = () => useEditorStore((store) => store.canvas); export const useSetCanvas = () => useEditorStore((store) => store.setCanvas); //TODO-매니페스트, 현재 선택된 노드의 레이아웃 상태 구독 훅 추가 필요 -> 오른쪽 사이드 바에서 실시간으로 변경되는 x,y좌표 렌더링 할때 필요 + +export const useGetDescendantIds = () => + useEditorStore((store) => store.getDescendantIds); From 8c0e7a2287e29a8aecc3e910dbf50ae6d250f2ab Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 19 Jan 2026 01:20:02 +0900 Subject: [PATCH 05/21] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EB=8B=A8=EC=88=9C?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/editor/src/components/{ => editor}/Canvas.tsx | 6 ++++-- .../src/components/{ => editor}/__tests__/Canvas.test.tsx | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) rename apps/editor/src/components/{ => editor}/Canvas.tsx (96%) rename apps/editor/src/components/{ => editor}/__tests__/Canvas.test.tsx (98%) diff --git a/apps/editor/src/components/Canvas.tsx b/apps/editor/src/components/editor/Canvas.tsx similarity index 96% rename from apps/editor/src/components/Canvas.tsx rename to apps/editor/src/components/editor/Canvas.tsx index 434baea..89a52cf 100644 --- a/apps/editor/src/components/Canvas.tsx +++ b/apps/editor/src/components/editor/Canvas.tsx @@ -1,9 +1,11 @@ +"use client"; + import { useCanvas, useCurNodes, useSelectedNodeId, useSelectNode, - useUpdateNode, + useUpdateNodeLayout, } from "@/stores/useEditorStore"; import EditorNodeWrapper from "@repo/ui/core/EditorNodeWrapper.jsx"; import NodeRenderer from "@repo/ui/core/NodeRenderer.jsx"; @@ -14,7 +16,7 @@ export default function Canvas() { const nodes = useCurNodes(); const selectedNodeId = useSelectedNodeId(); const selectNode = useSelectNode(); - const updateNode = useUpdateNode(); + const updateNode = useUpdateNodeLayout(); const canvasState = useCanvas(); //FIXME-각 노드들에 key속성 추가해주기. -> 리액트 경고 발생 diff --git a/apps/editor/src/components/__tests__/Canvas.test.tsx b/apps/editor/src/components/editor/__tests__/Canvas.test.tsx similarity index 98% rename from apps/editor/src/components/__tests__/Canvas.test.tsx rename to apps/editor/src/components/editor/__tests__/Canvas.test.tsx index 2f13928..ce3716c 100644 --- a/apps/editor/src/components/__tests__/Canvas.test.tsx +++ b/apps/editor/src/components/editor/__tests__/Canvas.test.tsx @@ -6,7 +6,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; const mockUseCurNodes = vi.fn(); const mockUseSelectedNodeId = vi.fn(); const mockUseSelectNode = vi.fn(); -const mockUseUpdateNode = vi.fn(); +const mockUseUpdateNodeLayout = vi.fn(); const mockUseCanvas = vi.fn(); const mockUseSetCanvas = vi.fn(); @@ -15,7 +15,7 @@ vi.mock("@/stores/useEditorStore", () => ({ useCurNodes: () => mockUseCurNodes(), //지연 실행 -> 이렇게 되면 다른 테스트에서 목함수의 반환값을 바꿔도 언제나 새롭게 해당 함수가 호출 되므로 다른 테스트의 영향을 받지 않는다. useSelectedNodeId: () => mockUseSelectedNodeId(), useSelectNode: () => mockUseSelectNode(), - useUpdateNode: () => mockUseUpdateNode(), + useUpdateNodeLayout: () => mockUseUpdateNodeLayout(), useCanvas: () => mockUseCanvas(), useSetCanvas: () => mockUseSetCanvas(), })); From f67beaa14127cb46ae2935f230bca92119130beb Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 19 Jan 2026 01:21:29 +0900 Subject: [PATCH 06/21] =?UTF-8?q?=E2=9C=A8=20nodes=20=EA=B0=80=EC=A0=B8?= =?UTF-8?q?=EC=98=A4=EB=8A=94=20=EC=84=9C=EB=B2=84=EC=95=A1=EC=85=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 현재는 목서버인 json-server에서 특정 페이지id에 해당하는 Node를 가져오도록 구현. --- .../src/actions/editor/getNodesFromDB.ts | 26 +++++++++++++++++++ packages/ui/src/types/nodes.ts | 2 ++ 2 files changed, 28 insertions(+) create mode 100644 apps/editor/src/actions/editor/getNodesFromDB.ts diff --git a/apps/editor/src/actions/editor/getNodesFromDB.ts b/apps/editor/src/actions/editor/getNodesFromDB.ts new file mode 100644 index 0000000..05a341b --- /dev/null +++ b/apps/editor/src/actions/editor/getNodesFromDB.ts @@ -0,0 +1,26 @@ +"use server"; + +import { WcxNode } from "types"; + +const BASE_URL = process.env.NEXT_PUBLIC_JSON_SERVER_URL; + +export default async function getNodesFromDB( + pageId: number, +): Promise { + try { + const res = await fetch(`${BASE_URL}/nodes?page_id=${pageId}`, { + cache: "no-store", //FIXME-캐시 설정을 Tag기반으로 수정해야함!! + }); + + if (!res.ok) { + throw new Error(`Failed to fetch nodes from JSON-Server:${res.status}`); + } + + const data = await res.json(); + + return data as WcxNode[]; + } catch (error) { + console.error("❌ Error fetching nodes:", error); + return []; + } +} diff --git a/packages/ui/src/types/nodes.ts b/packages/ui/src/types/nodes.ts index 88d8e6b..a67779c 100644 --- a/packages/ui/src/types/nodes.ts +++ b/packages/ui/src/types/nodes.ts @@ -7,6 +7,8 @@ import { NodeStyle } from "./styles"; // 1. 공통 필드 (모든 노드가 무조건 가지는 것) export interface BaseNode { id: string; //UUID + + //TODO-페이지id 타입 확실하게 정해야함 page_id: string | number; // 소속 페이지 parent_id: string | null; // 부모 노드 position: number; // 정렬 순서 From b02ad5e4c10a5f26c705e87514a87ca7f08e5d8a Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 19 Jan 2026 01:22:30 +0900 Subject: [PATCH 07/21] =?UTF-8?q?=E2=9C=A8=20=EC=97=90=EB=94=94=ED=84=B0?= =?UTF-8?q?=20=EC=8A=A4=ED=86=A0=EC=96=B4=EC=9D=98=20=EC=95=A1=EC=85=98=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=9B=85=20?= =?UTF-8?q?=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 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/editor/src/stores/useEditorStore.ts b/apps/editor/src/stores/useEditorStore.ts index 8d1c17d..5a86358 100644 --- a/apps/editor/src/stores/useEditorStore.ts +++ b/apps/editor/src/stores/useEditorStore.ts @@ -93,6 +93,11 @@ const useEditorStore = create( ); //TODO-각 커스텀훅 사용 설명 주석 달기 + +export const useSetNode = () => useEditorStore((store) => store.setNode); + +export const useAddNode = () => useEditorStore((store) => store.addNode); + export const useSelectedNodeId = () => useEditorStore((store) => store.selectedNodeId); From f551dae29153959fe237d406de3f02d6ed21abd4 Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 19 Jan 2026 01:23:56 +0900 Subject: [PATCH 08/21] =?UTF-8?q?=E2=9C=A8=20Editor=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EC=96=B4=20=EC=83=81=ED=83=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EB=9E=98=ED=8D=BC=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 서버액션에서 받은 nodes를 props로 받아 Editor스토어의 nodes 상태를 업데이트합니다. --- .../editor/EditorStoreInitializer.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 apps/editor/src/components/editor/EditorStoreInitializer.tsx diff --git a/apps/editor/src/components/editor/EditorStoreInitializer.tsx b/apps/editor/src/components/editor/EditorStoreInitializer.tsx new file mode 100644 index 0000000..c3c85ff --- /dev/null +++ b/apps/editor/src/components/editor/EditorStoreInitializer.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { useSetNode } from "@/stores/useEditorStore"; +import { useEffect } from "react"; +import { WcxNode } from "types"; + +interface EditorStoreInitializer { + children: React.ReactNode; + initialNodes: WcxNode[]; +} + +export default function EditorStoreInitializer({ + children, + initialNodes, +}: EditorStoreInitializer) { + const setNode = useSetNode(); + useEffect(() => { + setNode(initialNodes); + }, [initialNodes]); + + return <>{children}; +} From 1c3eb05c9e01bdb77a9a42a7ff4a4a98e11c3d23 Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 19 Jan 2026 11:46:45 +0900 Subject: [PATCH 09/21] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20EditorStoreInitializ?= =?UTF-8?q?er=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 초기에 리렌더링이 될때마다 스토어가 업데이트 되던 현상 수정 완료. 의존성배열의 값이 참조값이라 리렌더링이 될때마다 계속해서 스토어의 상태도 업데이트되던 문제 발생. useRef를 이용해 마운트 초기 1회만 서버액션에서 받아온 데이터로 업데이트 하고, 이후에는 클라이언트 측에서 데이터를 관리할 예정입니다. --- .../src/components/editor/EditorStoreInitializer.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/editor/src/components/editor/EditorStoreInitializer.tsx b/apps/editor/src/components/editor/EditorStoreInitializer.tsx index c3c85ff..ede4229 100644 --- a/apps/editor/src/components/editor/EditorStoreInitializer.tsx +++ b/apps/editor/src/components/editor/EditorStoreInitializer.tsx @@ -1,7 +1,7 @@ "use client"; import { useSetNode } from "@/stores/useEditorStore"; -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import { WcxNode } from "types"; interface EditorStoreInitializer { @@ -9,14 +9,18 @@ interface EditorStoreInitializer { initialNodes: WcxNode[]; } +//TODO-일단 DB와 연동하지 말자. 서버액션의 사용이유는 초기 렌더링 속도 향상임. 이후에는 클라이언트에서 스토어 업데이트를 할 예정. export default function EditorStoreInitializer({ children, initialNodes, }: EditorStoreInitializer) { + const initialized = useRef(false); const setNode = useSetNode(); useEffect(() => { + if (initialized.current) return; setNode(initialNodes); - }, [initialNodes]); + initialized.current = true; + }, [initialNodes, setNode]); return <>{children}; } From 33f78a7a56845a61dde619f84fbaca686127828b Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 19 Jan 2026 11:51:00 +0900 Subject: [PATCH 10/21] =?UTF-8?q?=E2=9C=A8=20Editor=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=8A=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/editor/src/app/editor/page.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 apps/editor/src/app/editor/page.tsx diff --git a/apps/editor/src/app/editor/page.tsx b/apps/editor/src/app/editor/page.tsx new file mode 100644 index 0000000..ced2bc0 --- /dev/null +++ b/apps/editor/src/app/editor/page.tsx @@ -0,0 +1,17 @@ +import getNodesFromDB from "@/actions/editor/getNodesFromDB"; +import Canvas from "@/components/editor/Canvas"; +import EditorStoreInitializer from "@/components/editor/EditorStoreInitializer"; + +export default async function EditorPage() { + const pageId = 201; //목데이터입니다. + //서버 액션 함수(nodes데이터 패칭함수) + const nodes = await getNodesFromDB(pageId); + + return ( + //클라이언트 스토어 업데이트 컴포넌트 실행 + + {/* 다른 에디터 관련 컴포넌트들은 이곳에서 렌더링 됩니다!(사이드바,매니패스트 수정 컴포넌트 등등...) */} + + + ); +} From 5a0db98ce0a0829118f8770172225d325f53e75e Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 19 Jan 2026 13:06:59 +0900 Subject: [PATCH 11/21] =?UTF-8?q?=E2=9C=A8=20=EB=85=B8=EB=93=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=95=A1=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 입력받은 NodeId를 통해 nodes상태에서 해당하는 노드를 찾아 삭제하는 액션 구현 완료 --- apps/editor/src/stores/useEditorStore.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/editor/src/stores/useEditorStore.ts b/apps/editor/src/stores/useEditorStore.ts index 5a86358..e1116eb 100644 --- a/apps/editor/src/stores/useEditorStore.ts +++ b/apps/editor/src/stores/useEditorStore.ts @@ -10,7 +10,7 @@ const useEditorStore = create( combine( { selectedNodeId: null as string | null, - nodes: null as null | WcxNode[], //TODO- 추후에 현재 페이지에 해당하는 노드들을 받아오는 로직을 통해 해당 상태가 업데이트 되야 한다. + nodes: null as null | WcxNode[], //TODO- 추후에 현재 페이지에 해당하는 노드들을 받아오는 로직을 통해 해당 상태가 업데이트 되야 한다. -> EditorStoreInitializer컴포넌트에서 담당 canvas: { dx: 0, dy: 0, scale: 1 }, }, (set, get) => ({ @@ -25,6 +25,16 @@ const useEditorStore = create( state.nodes?.push(node); }); }, + deleteNode(nodeId: string) { + set((state) => { + if (!state.nodes) return; + const targetNodeIdx = state.nodes?.findIndex( + (node) => node.id === nodeId, + ); + if (targetNodeIdx === -1) return; + state.nodes.splice(targetNodeIdx, 1); + }); + }, getDescendantIds(nodeId: string): string[] { const nodes = get().nodes; if (!nodes) return []; @@ -98,6 +108,8 @@ export const useSetNode = () => useEditorStore((store) => store.setNode); export const useAddNode = () => useEditorStore((store) => store.addNode); +export const useDeleteNode = () => useEditorStore((store) => store.deleteNode); + export const useSelectedNodeId = () => useEditorStore((store) => store.selectedNodeId); From ee3d8c47b1320efb11d8629255b8ddc1cb327cd6 Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 19 Jan 2026 13:49:41 +0900 Subject: [PATCH 12/21] =?UTF-8?q?=E2=9C=A8=20updateNode=20=EC=95=A1?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80=20=EA=B5=AC=ED=98=84,=20?= =?UTF-8?q?=EA=B0=81=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=9B=85=20JsDoc=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Node의 모든 속성을 통합 업데이트 하는 액션 추가 완료. 스토어에서 내보내는 모든 커스텀 훅에 대한 주석 추가 완료 --- apps/editor/src/stores/useEditorStore.ts | 91 ++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 4 deletions(-) diff --git a/apps/editor/src/stores/useEditorStore.ts b/apps/editor/src/stores/useEditorStore.ts index e1116eb..6953cce 100644 --- a/apps/editor/src/stores/useEditorStore.ts +++ b/apps/editor/src/stores/useEditorStore.ts @@ -75,7 +75,11 @@ const useEditorStore = create( "editorStore/clearNode", ); }, - //TODO-updateNode는 현재 오직 레이아웃(node.layout)만 변경이 가능하다. -> 액션 이름 수정 필요..? + /** + * 노드의 레이아웃(위치, 크기)을 업데이트합니다. + * @param targetNodeId - 업데이트할 노드의 ID + * @param updates - 변경할 레이아웃 속성 (Partial) + */ updateNodeLayout(targetNodeId: string, updates: Partial) { set((state) => { const targetNode = state.nodes?.find( @@ -87,6 +91,34 @@ const useEditorStore = create( }); }, + /** + * 노드의 모든 속성(props, style 등)을 통합 업데이트합니다. + * style, props는 얕은 병합(shallow merge)을 수행합니다. + * @param targetNodeId - 대상 노드 ID + * @param updates - 업데이트할 속성 객체 + */ + updateNode(targetNodeId: string, updates: Partial) { + set((state) => { + const targetNode = state.nodes?.find( + ({ id }) => id === targetNodeId, + ); + if (!targetNode) return; + + // 1. 최상위 속성 업데이트 (type 등) + // (주의: 객체 타입인 style, props, layout을 통째로 덮어쓰지 않도록 별도 처리) + const { style, props, layout, ...rest } = updates; + Object.assign(targetNode, rest); + + // 2. 하위 객체 병합 업데이트 + if (style) { + targetNode.style = { ...targetNode.style, ...style }; + } + if (props) { + targetNode.props = { ...targetNode.props, ...props }; + } + }); + }, + //TODO-노드 상세 데이터 수정하는 액션 추가 필요 setCanvas(updates: CanvasState) { set((state) => { @@ -102,31 +134,82 @@ const useEditorStore = create( ), ); -//TODO-각 커스텀훅 사용 설명 주석 달기 +// ------------------------------------------------------------------ +// Custom Hooks w/ JSDoc +// ------------------------------------------------------------------ +/** + * [Action] 캔버스의 노드 리스트를 초기화(설정)하는 함수를 반환합니다. + * @example + * const setNode = useSetNode(); + * setNode(initialNodes); + */ export const useSetNode = () => useEditorStore((store) => store.setNode); +/** + * [Action] 새로운 노드를 추가하는 함수를 반환합니다. + * @example + * const addNode = useAddNode(); + * addNode(newNodeObject); + */ export const useAddNode = () => useEditorStore((store) => store.addNode); +/** + * [Action] 특정 노드를 삭제하는 함수를 반환합니다. + * @param nodeId - 삭제할 노드의 ID + */ export const useDeleteNode = () => useEditorStore((store) => store.deleteNode); +/** + * [Selector] 현재 선택된 노드의 ID를 반환합니다. + * 선택된 노드가 없으면 null을 반환합니다. + */ export const useSelectedNodeId = () => useEditorStore((store) => store.selectedNodeId); +/** + * [Action] 특정 노드를 선택(포커스)하는 함수를 반환합니다. + * @param id - 선택할 노드의 ID + */ export const useSelectNode = () => useEditorStore((store) => store.selectNode); +/** + * [Action] 현재 선택된 노드 해제(선택 취소)하는 함수를 반환합니다. + */ export const useClearNode = () => useEditorStore((store) => store.clearNode); +/** + * [Selector] 현재 캔버스의 모든 노드 배열을 반환합니다. + * 노드가 없으면 null을 반환합니다. + */ export const useCurNodes = () => useEditorStore((store) => store.nodes); +/** + * [Action] 노드의 레이아웃(x, y, w, h 등)만을 빠르게 업데이트할 때 사용합니다. + * 드래그 앤 드롭 등 퍼포먼스가 중요한 경우 적합합니다. + */ export const useUpdateNodeLayout = () => useEditorStore((store) => store.updateNodeLayout); +/** + * [Action] 노드의 모든 속성(style, props)을 범용적으로 업데이트합니다. + * 속성 패널(Right Sidebar)에서 값을 수정할 때 사용하기 적합합니다. + */ +export const useUpdateNode = () => useEditorStore((store) => store.updateNode); + +/** + * [Selector] 캔버스의 뷰포트 상태(pan, scale)를 반환합니다. + */ export const useCanvas = () => useEditorStore((store) => store.canvas); +/** + * [Action] 캔버스의 뷰포트 상태(pan, scale)를 업데이트합니다. + */ export const useSetCanvas = () => useEditorStore((store) => store.setCanvas); -//TODO-매니페스트, 현재 선택된 노드의 레이아웃 상태 구독 훅 추가 필요 -> 오른쪽 사이드 바에서 실시간으로 변경되는 x,y좌표 렌더링 할때 필요 - +/** + * [Helper] 특정 노드의 모든 자손(Children) ID 배열을 가져오는 함수를 반환합니다. + * 재귀적으로 탐색합니다. + */ export const useGetDescendantIds = () => useEditorStore((store) => store.getDescendantIds); From 49e7d3afd0e134ca537fdf4eae978db7d5c4f409 Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 19 Jan 2026 15:47:43 +0900 Subject: [PATCH 13/21] =?UTF-8?q?=F0=9F=90=9B=20=ED=85=8C=EC=9D=BC?= =?UTF-8?q?=EC=9C=88=EB=93=9C=20=ED=8C=8C=EC=9D=BC=EC=9D=84=20Import?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=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 --- apps/editor/src/app/layout.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/editor/src/app/layout.tsx b/apps/editor/src/app/layout.tsx index d446f4e..e3e4f45 100644 --- a/apps/editor/src/app/layout.tsx +++ b/apps/editor/src/app/layout.tsx @@ -1,3 +1,4 @@ +import "../styles.css"; import QueryProvider from "src/providers/queryProvider"; export default function RootLayout({ From 773d90d6fd40639f7c4a1bcaac2a3e90813a0182 Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 19 Jan 2026 15:48:01 +0900 Subject: [PATCH 14/21] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EB=AA=A9=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EC=9D=98=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EA=B2=BD=EB=A1=9C=20=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 | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/apps/editor/db.json b/apps/editor/db.json index 2322c07..429b01b 100644 --- a/apps/editor/db.json +++ b/apps/editor/db.json @@ -61,7 +61,13 @@ "parent_id": "1002", "type": "Heading", "position": 0, - "layout": { "x": 100, "y": 50, "width": 1200, "height": 100, "zIndex": 2 }, + "layout": { + "x": 100, + "y": 50, + "width": 1200, + "height": 100, + "zIndex": 2 + }, "props": { "text": "주요 기능 소개", "level": "h2" @@ -82,7 +88,13 @@ "parent_id": "1002", "type": "Text", "position": 1, - "layout": { "x": 100, "y": 150, "width": 800, "height": 200, "zIndex": 2 }, + "layout": { + "x": 100, + "y": 150, + "width": 800, + "height": 200, + "zIndex": 2 + }, "props": { "text": "직관적인 드래그 앤 드롭 인터페이스로 코딩 없이 웹사이트를 완성하세요. 모든 컴포넌트는 사용자가 원하는 대로 커스터마이징할 수 있습니다." }, @@ -103,7 +115,13 @@ "parent_id": null, "type": "Container", "position": 2, - "layout": { "x": 0, "y": 1400, "width": 1440, "height": 600, "zIndex": 1 }, + "layout": { + "x": 0, + "y": 1400, + "width": 1440, + "height": 600, + "zIndex": 1 + }, "props": { "id": "gallery" }, "style": { "layout": { @@ -129,7 +147,7 @@ "position": 0, "layout": { "x": 50, "y": 50, "width": 600, "height": 300, "zIndex": 2 }, "props": { - "src": "https://images.example.com/gallery-1.jpg", + "src": "data:image/jpeg;base64,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", "alt": "갤러리 이미지 1" }, "style": { @@ -150,7 +168,7 @@ "position": 1, "layout": { "x": 700, "y": 50, "width": 600, "height": 300, "zIndex": 2 }, "props": { - "src": "https://images.example.com/gallery-2.jpg", + "src": "https://image.utoimage.com/preview/cp872722/2022/12/202212008462_500.jpg", "alt": "갤러리 이미지 2" }, "style": { @@ -209,7 +227,13 @@ "parent_id": null, "type": "Button", "position": 3, - "layout": { "x": 600, "y": 2100, "width": 200, "height": 60, "zIndex": 1 }, + "layout": { + "x": 600, + "y": 2100, + "width": 200, + "height": 60, + "zIndex": 1 + }, "props": { "text": "더 알아보기", "link": "/features" From ac0910f07586496d89dc170a59b5c0eeacd7dea1 Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 19 Jan 2026 15:50:15 +0900 Subject: [PATCH 15/21] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 개발 환경에서 외부의 모든 파일을 가져 올 수 있도록 와일드 카드 설정 완료. json서버 실행 스크립트 추가. @repo/ui를 editor/apps에서 사용할 수 있도록 tsconfig에 경로 추가 --- apps/editor/next.config.ts | 8 ++++++++ apps/editor/package.json | 1 + apps/editor/tsconfig.json | 1 + 3 files changed, 10 insertions(+) diff --git a/apps/editor/next.config.ts b/apps/editor/next.config.ts index 44d101d..1d6b8f1 100644 --- a/apps/editor/next.config.ts +++ b/apps/editor/next.config.ts @@ -3,6 +3,14 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", // 배포에 필요한 파일만 모아줍니다 transpilePackages: ["@repo/ui"], + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "**", // 개발 편의를 위해 모든 도메인 허용 (운영 배포시에는 특정 도메인으로 제한) + }, + ], + }, }; export default nextConfig; diff --git a/apps/editor/package.json b/apps/editor/package.json index 14bce7e..334bf83 100644 --- a/apps/editor/package.json +++ b/apps/editor/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "dev": "next dev --turbopack", + "db": "json-server db.json --port 4000", "build": "next build --turbopack", "start": "next start", "lint": "next lint", diff --git a/apps/editor/tsconfig.json b/apps/editor/tsconfig.json index 8d5455d..150dc05 100644 --- a/apps/editor/tsconfig.json +++ b/apps/editor/tsconfig.json @@ -20,6 +20,7 @@ } ], "paths": { + "@repo/ui/*": ["../../packages/ui/src/*"], "@/*": ["src/*"], "@editor/*": ["src/packages/editor/*"], "@components/*": ["src/packages/editor/components/*"], From 713a1c6c315c731ad6143766e35e6edd33693eef Mon Sep 17 00:00:00 2001 From: y-minion Date: Tue, 20 Jan 2026 18:49:17 +0900 Subject: [PATCH 16/21] =?UTF-8?q?=E2=9C=A8=20=EA=B8=B0=EB=B3=B8=20Cavas?= =?UTF-8?q?=EC=9C=84=EC=A0=AF=20dev=ED=99=98=EA=B2=BD=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/editor/src/app/editor/page.tsx | 17 ++++++++++++----- apps/editor/src/app/page.tsx | 2 ++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/editor/src/app/editor/page.tsx b/apps/editor/src/app/editor/page.tsx index ced2bc0..24e7afd 100644 --- a/apps/editor/src/app/editor/page.tsx +++ b/apps/editor/src/app/editor/page.tsx @@ -1,6 +1,7 @@ import getNodesFromDB from "@/actions/editor/getNodesFromDB"; import Canvas from "@/components/editor/Canvas"; import EditorStoreInitializer from "@/components/editor/EditorStoreInitializer"; +import { RuntimeProvider } from "@repo/ui/context/runtimeContext"; export default async function EditorPage() { const pageId = 201; //목데이터입니다. @@ -8,10 +9,16 @@ export default async function EditorPage() { const nodes = await getNodesFromDB(pageId); return ( - //클라이언트 스토어 업데이트 컴포넌트 실행 - - {/* 다른 에디터 관련 컴포넌트들은 이곳에서 렌더링 됩니다!(사이드바,매니패스트 수정 컴포넌트 등등...) */} - - +
+

에디터 페이지 입니다.

+ + {/* 다른 에디터 관련 컴포넌트들은 이곳에서 렌더링 됩니다!(사이드바,매니패스트 수정 컴포넌트 등등...) */} + +
+ +
+
+
+
); } diff --git a/apps/editor/src/app/page.tsx b/apps/editor/src/app/page.tsx index 22c9959..f88c97a 100644 --- a/apps/editor/src/app/page.tsx +++ b/apps/editor/src/app/page.tsx @@ -5,6 +5,8 @@ export default function Home() {

환경합니다

테스트 환경 +
+ 에디터 페이지
); } From d856d04ec536bdd9d0c4e55d1363362598d9f477 Mon Sep 17 00:00:00 2001 From: y-minion Date: Tue, 20 Jan 2026 18:51:20 +0900 Subject: [PATCH 17/21] =?UTF-8?q?=F0=9F=92=84=20css=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=B0=B8=EC=A1=B0=20=EC=88=98=EC=A0=95,=20=EA=B2=A9=EC=9E=90?= =?UTF-8?q?=20=EC=9C=A0=ED=8B=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit canvas에서 보여줄 격자 가이드 라인 유틸리티 추가 --- apps/editor/src/styles.css | 1 + packages/ui/theme.css | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/apps/editor/src/styles.css b/apps/editor/src/styles.css index f801e2d..a5ad19b 100644 --- a/apps/editor/src/styles.css +++ b/apps/editor/src/styles.css @@ -1,2 +1,3 @@ @import "tailwindcss"; +@source "../../../packages/ui"; @import "../../../packages/ui/theme.css"; diff --git a/packages/ui/theme.css b/packages/ui/theme.css index 33f4ba9..fe8983a 100644 --- a/packages/ui/theme.css +++ b/packages/ui/theme.css @@ -16,3 +16,10 @@ --color-semantic-error: #ef4444; --color-semantic-info: #3b82f6; } + +@utility bg-grid-pattern { + background-size: 20px 20px; + background-image: + linear-gradient(to right, #ddd 1px, transparent 1px), + linear-gradient(to bottom, #ddd 1px, transparent 1px); +} From 2cf0150dbe4efe9a4fafec2a3834a7a75b4c52ce Mon Sep 17 00:00:00 2001 From: y-minion Date: Tue, 20 Jan 2026 19:03:06 +0900 Subject: [PATCH 18/21] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이미지의 ghost 드래그 방지위해 속성 추가 - 자잘한 수정사항들 수정 완료 --- apps/editor/src/app/editor/page.tsx | 6 +++--- packages/ui/src/components/Image.tsx | 14 ++++++++++++-- packages/ui/src/core/EditorNodeWrapper.tsx | 10 +++++----- packages/ui/src/hooks/useActionHandler.tsx | 6 ++++-- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/apps/editor/src/app/editor/page.tsx b/apps/editor/src/app/editor/page.tsx index 24e7afd..4df8eb4 100644 --- a/apps/editor/src/app/editor/page.tsx +++ b/apps/editor/src/app/editor/page.tsx @@ -9,12 +9,12 @@ export default async function EditorPage() { const nodes = await getNodesFromDB(pageId); return ( -
-

에디터 페이지 입니다.

+
+

에디터 페이지 입니다.

{/* 다른 에디터 관련 컴포넌트들은 이곳에서 렌더링 됩니다!(사이드바,매니패스트 수정 컴포넌트 등등...) */} -
+
diff --git a/packages/ui/src/components/Image.tsx b/packages/ui/src/components/Image.tsx index 807d603..bfeb36d 100644 --- a/packages/ui/src/components/Image.tsx +++ b/packages/ui/src/components/Image.tsx @@ -16,9 +16,19 @@ export default function ImageComponent({
- {alt} + {alt} {caption &&
{caption}
} {children}
diff --git a/packages/ui/src/core/EditorNodeWrapper.tsx b/packages/ui/src/core/EditorNodeWrapper.tsx index 1f18503..b761d62 100644 --- a/packages/ui/src/core/EditorNodeWrapper.tsx +++ b/packages/ui/src/core/EditorNodeWrapper.tsx @@ -69,16 +69,16 @@ export default function EditorNodeWrapper({ className={clsx("group cursor-pointer", isSelected && "z-50")} resizeHandleClasses={{ bottomLeft: isSelected - ? clsx(selectedNodeGuideClasses.handle, "-left-1 -bottom-1") + ? clsx(selectedNodeGuideClasses.handle, "!-left-1 !-bottom-1") : undefined, bottomRight: isSelected - ? clsx(selectedNodeGuideClasses.handle, "-right-1 -bottom-1") + ? clsx(selectedNodeGuideClasses.handle, "!-right-1 !-bottom-1") : undefined, topLeft: isSelected - ? clsx(selectedNodeGuideClasses.handle, "-left-1 -top-1") + ? clsx(selectedNodeGuideClasses.handle, "!-left-1 !-top-1") : undefined, topRight: isSelected - ? clsx(selectedNodeGuideClasses.handle, "-right-1 -top-1") + ? clsx(selectedNodeGuideClasses.handle, "!-right-1 !-top-1") : undefined, }} > @@ -89,7 +89,7 @@ export default function EditorNodeWrapper({ }} style={wrapperStyle} className={clsx( - "transition-shadow duration-200", + "h-full w-full transition-shadow duration-200", isSelected && selectedNodeGuideClasses.outline, )} > diff --git a/packages/ui/src/hooks/useActionHandler.tsx b/packages/ui/src/hooks/useActionHandler.tsx index e4f02d7..7c34ffc 100644 --- a/packages/ui/src/hooks/useActionHandler.tsx +++ b/packages/ui/src/hooks/useActionHandler.tsx @@ -1,10 +1,12 @@ -"use Rounter"; +"use client"; import { useRuntimeState } from "context/runtimeContext"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; + import { NodeAction } from "types/nodeAction"; export function useActionHandler(action?: NodeAction) { + //FIXME-추후에 useRouter훅을 주입받아서 사용해야할까? (@repo/ui가 앱라우터로 빌드되는게 올바른가? 리액트로만 만들어도 될것같은 생각.) const router = useRouter(); const { openModal } = useRuntimeState(); From f3ad53a3d470d28429ec544d2352d84d6b564e35 Mon Sep 17 00:00:00 2001 From: y-minion Date: Tue, 20 Jan 2026 19:05:25 +0900 Subject: [PATCH 19/21] =?UTF-8?q?=E2=9C=A8=20=ED=9C=A0=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=9C=A0?= =?UTF-8?q?=ED=8B=B8=EB=A6=AC=ED=8B=B0=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/utils/editor/handleWheel.ts | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 apps/editor/src/utils/editor/handleWheel.ts diff --git a/apps/editor/src/utils/editor/handleWheel.ts b/apps/editor/src/utils/editor/handleWheel.ts new file mode 100644 index 0000000..23441b3 --- /dev/null +++ b/apps/editor/src/utils/editor/handleWheel.ts @@ -0,0 +1,37 @@ +import { CanvasState } from "@repo/ui/types/rnd"; + +interface handleWheelParam { + e: React.WheelEvent; + canvas: CanvasState; + setCanvas: (updates: Partial) => void; +} + +export default function handleWheel({ + e, + canvas, + setCanvas, +}: handleWheelParam) { + // 1. "컨트롤 키(Win)나 커맨드 키(Mac)를 누른 채" 휠을 돌렸나? + if (e.ctrlKey || e.metaKey) { + // -> 줌(Zoom) 모드 + e.preventDefault(); + + //마우스가 너무 예민해서, 그 값을 1/1000로 확 줄여서 조금씩만 줌인/줌아웃 되도록 조절해 주는 안전장치. + const zoomSensitivity = 0.0001; + + // 계산된 새로운 배율로 업데이트 + const newScale = Math.min( + Math.max(0.1, canvas.scale - e.deltaY * zoomSensitivity), + 5, + ); + setCanvas({ scale: newScale }); + } else { + // 2. 키 안 누르고 그냥 휠만 돌렸나? + // -> 이동(Pan) 모드. + //스크롤을 생각하면 안되고, 캔버스의 transform을 생각해야한다! -> 이동 시키는 것! + setCanvas({ + dx: canvas.dx - e.deltaX, + dy: canvas.dy - e.deltaY, + }); + } +} From 04af7dca620ccf6ac71960ba45068447f439f078 Mon Sep 17 00:00:00 2001 From: y-minion Date: Tue, 20 Jan 2026 19:07:03 +0900 Subject: [PATCH 20/21] =?UTF-8?q?=E2=9C=A8=20=EC=BA=94=EB=B2=84=EC=8A=A4?= =?UTF-8?q?=20=ED=8C=A8=EB=8B=9D=20=EC=9C=A0=ED=8B=B8=EB=A6=AC=ED=8B=B0=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 각 이벤트에 따라 캔버스 패닝이 가능하도록 구현 완료. useRef와 여러 훅들을 매개변수로 받아 유틸리티 함수에서 사용할 수 있도록 구현 완료. --- .../src/utils/editor/canvasMouseHandler.ts | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 apps/editor/src/utils/editor/canvasMouseHandler.ts diff --git a/apps/editor/src/utils/editor/canvasMouseHandler.ts b/apps/editor/src/utils/editor/canvasMouseHandler.ts new file mode 100644 index 0000000..91fb132 --- /dev/null +++ b/apps/editor/src/utils/editor/canvasMouseHandler.ts @@ -0,0 +1,61 @@ +//캔버스 위젯에서 발생하는 마우스 이벤트를 다루는 유틸리티 함수 파일입니다. + +import { CanvasState } from "@repo/ui/types/rnd"; + +type IsPanning = React.RefObject; +type LastMousePos = React.RefObject<{ x: number; y: number }>; + +interface handleMouseDown { + e: React.MouseEvent; + isPanning: IsPanning; + lastMousePos: LastMousePos; + selectNode: (id: string | null) => void; +} + +interface handleMouseMove { + e: React.MouseEvent; + isPanning: IsPanning; + lastMousePos: LastMousePos; + setCanvas: (updates: Partial) => void; + canvasState: { + dx: number; + dy: number; + scale: number; + }; +} + +export function handleMouseDown({ + e, + isPanning, + lastMousePos, + selectNode, +}: handleMouseDown) { + if (e.button === 0) { + selectNode(null); + return; + } + e.preventDefault(); + isPanning.current = true; + lastMousePos.current = { + x: e.clientX, + y: e.clientY, + }; +} + +export function handleMouseUp({ isPanning }: { isPanning: IsPanning }) { + isPanning.current = false; +} + +export function handleMouseMove({ + e, + isPanning, + lastMousePos, + setCanvas, + canvasState, +}: handleMouseMove) { + if (!isPanning.current) return; + const deltaX = e.clientX - lastMousePos.current.x; + const deltaY = e.clientY - lastMousePos.current.y; + setCanvas({ dx: canvasState.dx + deltaX, dy: canvasState.dy + deltaY }); + lastMousePos.current = { x: e.clientX, y: e.clientY }; +} From 835b4801edd729196ff4651fc65114ea652c5a24 Mon Sep 17 00:00:00 2001 From: y-minion Date: Tue, 20 Jan 2026 19:08:33 +0900 Subject: [PATCH 21/21] =?UTF-8?q?=E2=9C=A8=20=EC=9C=A0=ED=8B=B8=EB=A6=AC?= =?UTF-8?q?=ED=8B=B0=20=ED=95=A8=EC=88=98=EC=99=80=20=EC=BA=94=EB=B2=84?= =?UTF-8?q?=EC=8A=A4=20=EC=97=B0=EA=B2=B0=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 캔버스를 조작하는 유틸리티들을 캔버스 위젯과 연동 완료. 이때 canvas의 크기가 보장이 안되서 노드들이 보이지 않던 에러가 있었으나, 캔버스 컨테이너의 크기를 보장 함으로써 노드 렌더링 버그를 해결 완료했음. --- apps/editor/src/components/editor/Canvas.tsx | 50 ++++++++++++++++++-- apps/editor/src/stores/useEditorStore.ts | 4 +- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/apps/editor/src/components/editor/Canvas.tsx b/apps/editor/src/components/editor/Canvas.tsx index 89a52cf..d675dfa 100644 --- a/apps/editor/src/components/editor/Canvas.tsx +++ b/apps/editor/src/components/editor/Canvas.tsx @@ -5,12 +5,19 @@ import { useCurNodes, useSelectedNodeId, useSelectNode, + useSetCanvas, useUpdateNodeLayout, } from "@/stores/useEditorStore"; -import EditorNodeWrapper from "@repo/ui/core/EditorNodeWrapper.jsx"; -import NodeRenderer from "@repo/ui/core/NodeRenderer.jsx"; -import { WcxNode } from "@repo/ui/types/nodes.js"; -import React from "react"; +import { + handleMouseDown, + handleMouseMove, + handleMouseUp, +} from "@/utils/editor/canvasMouseHandler"; +import handleWheel from "@/utils/editor/handleWheel"; +import EditorNodeWrapper from "@repo/ui/core/EditorNodeWrapper"; +import NodeRenderer from "@repo/ui/core/NodeRenderer"; +import { WcxNode } from "@repo/ui/types/nodes"; +import React, { useRef } from "react"; export default function Canvas() { const nodes = useCurNodes(); @@ -18,6 +25,10 @@ export default function Canvas() { const selectNode = useSelectNode(); const updateNode = useUpdateNodeLayout(); const canvasState = useCanvas(); + const setCanvas = useSetCanvas(); + + const isPanning = useRef(false); + const lastMousePos = useRef({ x: 0, y: 0 }); //FIXME-각 노드들에 key속성 추가해주기. -> 리액트 경고 발생 //FIXME-nodes가 비어있는 상황에서 에러발생. -> Base Condition에 Root가 들어간다.(Root는 단지 더미 노드일뿐 로직에 들어가면 안된다.) @@ -76,5 +87,34 @@ export default function Canvas() { ); } - return
{renderTree({ id: null })}
; + return ( +
handleWheel({ canvas: canvasState, e, setCanvas })} + onMouseDown={(e) => + handleMouseDown({ e, isPanning, lastMousePos, selectNode }) + } + onMouseMove={(e) => + handleMouseMove({ e, isPanning, lastMousePos, setCanvas, canvasState }) + } + onMouseUp={() => handleMouseUp({ isPanning })} + onMouseLeave={() => handleMouseUp({ isPanning })} + > +
+ {/* 배경 격자 (Helper Grid) */} +
+
+ {renderTree({ id: null })} +
+
+
+ ); } diff --git a/apps/editor/src/stores/useEditorStore.ts b/apps/editor/src/stores/useEditorStore.ts index 6953cce..1b4c1e6 100644 --- a/apps/editor/src/stores/useEditorStore.ts +++ b/apps/editor/src/stores/useEditorStore.ts @@ -57,7 +57,7 @@ const useEditorStore = create( return res; //TODO- 재귀 삭제 함수로 추출된 노드id는 deleteNodes에 담겨 있다. 이 데이터를 바탕으로 DB수정 시도 }, - selectNode(id: string) { + selectNode(id: string | null) { set( (state) => { state.selectedNodeId = id; @@ -120,7 +120,7 @@ const useEditorStore = create( }, //TODO-노드 상세 데이터 수정하는 액션 추가 필요 - setCanvas(updates: CanvasState) { + setCanvas(updates: Partial) { set((state) => { state.canvas = { ...state.canvas, ...updates }; });