From 7ce50c329cf42b061915d1f8ecd8a0899af0c55b Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 5 Jan 2026 10:24:46 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EB=85=B8=EB=93=9C?= =?UTF-8?q?=EC=9D=98=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=86=8D?= =?UTF-8?q?=EC=84=B1=EC=9D=80=20=EB=85=B8=EB=93=9C=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=9F=AC=EC=97=90=20=EB=93=A4=EC=96=B4=20=EA=B0=80=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8F=84=EB=A1=9D=20=ED=95=84=ED=84=B0=EB=A7=81=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 노드 렌더러는 언제나 래퍼 컴포넌트를 100%채우도록 렌더링 되야 하는데, 레이아웃 관련 CSS속성이 들어가면 스타일 중첩 발생으로 의도치 않게 렌더링 될 수 있음. 이를 방지 하기 위해 레이아웃 관련 속성은 노드 렌더러에 반영되지 않도록 필터링 적용 --- packages/ui/src/utils/applyStyles.ts | 29 +++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/utils/applyStyles.ts b/packages/ui/src/utils/applyStyles.ts index b4d9c0c..0683ee2 100644 --- a/packages/ui/src/utils/applyStyles.ts +++ b/packages/ui/src/utils/applyStyles.ts @@ -16,17 +16,36 @@ export default function applyStyles( ): CSSProperties | undefined { if (!styleData) return; - const combinedStyles = {}; + const combinedStyles: Record = {}; + + // Wrapper(부모)가 제어해야 할 레이아웃 속성 목록 (블랙리스트) + const LAYOUT_PROPERTIES = new Set([ + "width", + "height", + "position", + "top", + "bottom", + "left", + "right", + "zIndex", + "transform", + "margin", // 마진도 레이아웃에 영향을 주므로 제외하는 것이 안전함 + ]); + for (const key in styleData) { //카테고리별로 중첩된 스타일 데이터를 평탄화 시킴. //⭐️ className은 평탄화 작업에서 안전하게 제외합니다. if (key !== "className" && typeof styleData[key] === "object") { - Object.assign(combinedStyles, styleData[key]); - //스프레드 연산자 오버헤드 위험 + const categoryStyles = styleData[key] as Record; + + for (const styleKey in categoryStyles) { + // 레이아웃 속성이면 건너뜀 (Wrapper가 담당) + if (LAYOUT_PROPERTIES.has(styleKey)) continue; + + combinedStyles[styleKey] = categoryStyles[styleKey]; + } } } - // ... (다른 특수 CSS 속성 변환 로직 추후 구현) ... - return combinedStyles; } From ab9a74993694c4c899843fc35c4005e1c27a7baa Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 5 Jan 2026 19:15:48 +0900 Subject: [PATCH 2/6] =?UTF-8?q?@repo/ui=20=EC=9D=98=20=ED=95=98=EC=9C=84?= =?UTF-8?q?=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20export=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 모든 하위 디렉토리들이 다른 애플리케이션에서 참조될 수 있도록 수정 완료 --- packages/ui/package.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index 45b7be5..f1e4c52 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -2,7 +2,8 @@ "name": "@repo/ui", "version": "0.0.0", "exports": { - "./renderer": "./src/renderer/NodeRenderer.tsx" + "./renderer": "./src/renderer/NodeRenderer.tsx", + "./*": "./src/*" }, "scripts": { "type-check": "tsc --noEmit" @@ -18,6 +19,8 @@ "typescript": "^5" }, "dependencies": { - "framer-motion": "^12.23.26" + "clsx": "^2.1.1", + "framer-motion": "^12.23.26", + "react-rnd": "^10.5.2" } } From f4a8943ace9e547582564a84ee1050f9208affd7 Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 7 Jan 2026 19:20:32 +0900 Subject: [PATCH 3/6] =?UTF-8?q?=EA=B0=9C=EB=B0=9C=20=ED=99=98=EA=B2=BD=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 테일윈드 사용시 모노레포 구조에서 프로젝트 위치 찾을 수 있도록 설정파일 생성, immer 라이브러리 설치. --- .vscode/settings.json | 23 ++++++++++ apps/editor/package.json | 1 + apps/editor/src/styles.css | 1 + packages/ui/package.json | 3 ++ packages/ui/postcss.config.mjs | 5 ++ packages/ui/src/styles.css | 1 + packages/ui/tailwind.config.js | 10 ++++ pnpm-lock.yaml | 83 +++++++++++++++++++++++++++++++++- 8 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 apps/editor/src/styles.css create mode 100644 packages/ui/postcss.config.mjs create mode 100644 packages/ui/src/styles.css create mode 100644 packages/ui/tailwind.config.js diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d1d3a94 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "editor.quickSuggestions": { + "strings": true + }, + "css.validate": false, + "tailwindCSS.includeLanguages": { + "typescript": "javascript", + "typescriptreact": "javascript" + }, + "files.associations": { + "*.css": "tailwindcss" + }, + "tailwindCSS.experimental.classRegex": [ + ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], + ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"], + ["clsx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"], + [ + "resizeHandleClasses\\s*=\\s*\\{\\{([\\s\\S]*?)\\}\\}", + "[\"'`]([^\"'`]*).*?[\"'`]" + ] + ], + "todo-tree.tree.disableCompactFolders": true +} diff --git a/apps/editor/package.json b/apps/editor/package.json index e2d9611..7f23dda 100644 --- a/apps/editor/package.json +++ b/apps/editor/package.json @@ -13,6 +13,7 @@ "dependencies": { "@repo/ui": "workspace:*", "@tanstack/react-query": "^5.90.8", + "immer": "^11.1.3", "json-server": "^1.0.0-beta.3", "next": "15.5.9", "react": "19.1.0", diff --git a/apps/editor/src/styles.css b/apps/editor/src/styles.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/apps/editor/src/styles.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/packages/ui/package.json b/packages/ui/package.json index f1e4c52..4c6e5a1 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -13,9 +13,12 @@ "react": "19.1.0" }, "devDependencies": { + "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "postcss": "^8.5.6", + "tailwindcss": "^4", "typescript": "^5" }, "dependencies": { diff --git a/packages/ui/postcss.config.mjs b/packages/ui/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/packages/ui/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/packages/ui/src/styles.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/packages/ui/tailwind.config.js b/packages/ui/tailwind.config.js new file mode 100644 index 0000000..af60256 --- /dev/null +++ b/packages/ui/tailwind.config.js @@ -0,0 +1,10 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./src/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 576aab3..686d608 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@tanstack/react-query': specifier: ^5.90.8 version: 5.90.10(react@19.1.0) + immer: + specifier: ^11.1.3 + version: 11.1.3 json-server: specifier: ^1.0.0-beta.3 version: 1.0.0-beta.3 @@ -34,7 +37,7 @@ importers: version: 19.1.0(react@19.1.0) zustand: specifier: ^5.0.8 - version: 5.0.8(@types/react@19.2.6)(react@19.1.0) + version: 5.0.8(@types/react@19.2.6)(immer@11.1.3)(react@19.1.0) devDependencies: '@eslint/eslintrc': specifier: ^3 @@ -87,6 +90,9 @@ importers: packages/ui: dependencies: + clsx: + specifier: ^2.1.1 + version: 2.1.1 framer-motion: specifier: ^12.23.26 version: 12.23.26(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -96,7 +102,13 @@ importers: react: specifier: 19.1.0 version: 19.1.0 + react-rnd: + specifier: ^10.5.2 + version: 10.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) devDependencies: + '@tailwindcss/postcss': + specifier: ^4 + version: 4.1.17 '@types/node': specifier: ^20 version: 20.19.25 @@ -106,6 +118,12 @@ importers: '@types/react-dom': specifier: ^19 version: 19.2.3(@types/react@19.2.6) + postcss: + specifier: ^8.5.6 + version: 8.5.6 + tailwindcss: + specifier: ^4 + version: 4.1.17 typescript: specifier: ^5 version: 5.9.3 @@ -902,6 +920,14 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1317,6 +1343,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immer@11.1.3: + resolution: {integrity: sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1832,14 +1861,32 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + re-resizable@6.11.2: + resolution: {integrity: sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==} + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom@19.1.0: resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} peerDependencies: react: ^19.1.0 + react-draggable@4.4.6: + resolution: {integrity: sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==} + peerDependencies: + react: '>= 16.3.0' + react-dom: '>= 16.3.0' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-rnd@10.5.2: + resolution: {integrity: sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw==} + peerDependencies: + react: '>=16.3.0' + react-dom: '>=16.3.0' + react@19.1.0: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} @@ -2050,6 +2097,9 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2926,6 +2976,10 @@ snapshots: client-only@0.0.1: {} + clsx@1.2.1: {} + + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3473,6 +3527,8 @@ snapshots: ignore@7.0.5: {} + immer@11.1.3: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -3917,13 +3973,33 @@ snapshots: queue-microtask@1.2.3: {} + re-resizable@6.11.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-dom@19.1.0(react@19.1.0): dependencies: react: 19.1.0 scheduler: 0.26.0 + react-draggable@4.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + clsx: 1.2.1 + prop-types: 15.8.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-is@16.13.1: {} + react-rnd@10.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + re-resizable: 6.11.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-draggable: 4.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + tslib: 2.6.2 + react@19.1.0: {} readdirp@4.1.2: {} @@ -4197,6 +4273,8 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tslib@2.6.2: {} + tslib@2.8.1: {} turbo-darwin-64@2.6.1: @@ -4353,7 +4431,8 @@ snapshots: yocto-queue@0.1.0: {} - zustand@5.0.8(@types/react@19.2.6)(react@19.1.0): + zustand@5.0.8(@types/react@19.2.6)(immer@11.1.3)(react@19.1.0): optionalDependencies: '@types/react': 19.2.6 + immer: 11.1.3 react: 19.1.0 From dc6099a79e951076a036848ccf1f03e767b71a3e Mon Sep 17 00:00:00 2001 From: y-minion Date: Thu, 8 Jan 2026 11:22:30 +0900 Subject: [PATCH 4/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EB=AA=A9=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rnd고려해 노드의 layout키에 노드의 좌표값이 추가됨에 따라 데이터 수정. --- apps/editor/db.json | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/apps/editor/db.json b/apps/editor/db.json index d16b921..2322c07 100644 --- a/apps/editor/db.json +++ b/apps/editor/db.json @@ -1,11 +1,12 @@ { "nodes": [ { - "id": 1001, + "id": "1001", "page_id": 201, "parent_id": null, "type": "Hero", "position": 0, + "layout": { "x": 0, "y": 0, "width": 1440, "height": 600, "zIndex": 1 }, "props": { "heading": "WebCreator-X에 오신 것을 환영합니다", "subheading": "세상에 하나뿐인 당신의 웹사이트를 만들어보세요.", @@ -35,11 +36,12 @@ "created_at": "2025-11-13T06:18:00Z" }, { - "id": 1002, + "id": "1002", "page_id": 201, "parent_id": null, "type": "Container", "position": 1, + "layout": { "x": 0, "y": 600, "width": 1440, "height": 800, "zIndex": 1 }, "props": {}, "style": { "layout": { "display": "block" }, @@ -54,11 +56,12 @@ "created_at": "2025-11-13T06:20:00Z" }, { - "id": 1003, + "id": "1003", "page_id": 201, - "parent_id": 1002, + "parent_id": "1002", "type": "Heading", "position": 0, + "layout": { "x": 100, "y": 50, "width": 1200, "height": 100, "zIndex": 2 }, "props": { "text": "주요 기능 소개", "level": "h2" @@ -74,11 +77,12 @@ "created_at": "2025-11-13T06:21:00Z" }, { - "id": 1004, + "id": "1004", "page_id": 201, - "parent_id": 1002, + "parent_id": "1002", "type": "Text", "position": 1, + "layout": { "x": 100, "y": 150, "width": 800, "height": 200, "zIndex": 2 }, "props": { "text": "직관적인 드래그 앤 드롭 인터페이스로 코딩 없이 웹사이트를 완성하세요. 모든 컴포넌트는 사용자가 원하는 대로 커스터마이징할 수 있습니다." }, @@ -94,11 +98,12 @@ "created_at": "2025-11-13T06:22:00Z" }, { - "id": 1005, + "id": "1005", "page_id": 201, "parent_id": null, "type": "Container", "position": 2, + "layout": { "x": 0, "y": 1400, "width": 1440, "height": 600, "zIndex": 1 }, "props": { "id": "gallery" }, "style": { "layout": { @@ -117,11 +122,12 @@ "created_at": "2025-11-13T06:23:00Z" }, { - "id": 1006, + "id": "1006", "page_id": 201, - "parent_id": 1005, + "parent_id": "1005", "type": "Image", "position": 0, + "layout": { "x": 50, "y": 50, "width": 600, "height": 300, "zIndex": 2 }, "props": { "src": "https://images.example.com/gallery-1.jpg", "alt": "갤러리 이미지 1" @@ -137,11 +143,12 @@ "created_at": "2025-11-13T06:24:00Z" }, { - "id": 1007, + "id": "1007", "page_id": 201, - "parent_id": 1005, + "parent_id": "1005", "type": "Image", "position": 1, + "layout": { "x": 700, "y": 50, "width": 600, "height": 300, "zIndex": 2 }, "props": { "src": "https://images.example.com/gallery-2.jpg", "alt": "갤러리 이미지 2" @@ -157,11 +164,12 @@ "created_at": "2025-11-13T06:25:00Z" }, { - "id": 1008, + "id": "1008", "page_id": 201, - "parent_id": 1005, + "parent_id": "1005", "type": "Text", "position": 2, + "layout": { "x": 50, "y": 360, "width": 600, "height": 50, "zIndex": 2 }, "props": { "text": "이미지 설명 1" }, @@ -176,11 +184,12 @@ "created_at": "2025-11-13T06:26:00Z" }, { - "id": 1009, + "id": "1009", "page_id": 201, - "parent_id": 1005, + "parent_id": "1005", "type": "Text", "position": 3, + "layout": { "x": 700, "y": 360, "width": 600, "height": 50, "zIndex": 2 }, "props": { "text": "이미지 설명 2" }, @@ -195,11 +204,12 @@ "created_at": "2025-11-13T06:27:00Z" }, { - "id": 1010, + "id": "1010", "page_id": 201, "parent_id": null, "type": "Button", "position": 3, + "layout": { "x": 600, "y": 2100, "width": 200, "height": 60, "zIndex": 1 }, "props": { "text": "더 알아보기", "link": "/features" From 480f570af95469057ff2e8476c3e284c47b338b2 Mon Sep 17 00:00:00 2001 From: y-minion Date: Thu, 8 Jan 2026 11:24:17 +0900 Subject: [PATCH 5/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EC=97=90=EB=94=94?= =?UTF-8?q?=ED=84=B0=20=EB=AA=A8=EB=93=9C=20=EC=A0=84=EC=9A=A9=20=EB=85=B8?= =?UTF-8?q?=EB=93=9C=20=EB=A0=8C=EB=8D=94=EB=9F=AC=20=EB=9E=98=ED=8D=BC=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 모든 노드를 Dnd, 리사이징 하기 위해 Rnd라이브러리 채택. Rnd라이브러리를 사용하기 위해 기존의 래퍼 컴포넌트를 Rnd컴포넌트로 감싸서 노드 렌더러들이 이동, 리사이징 가능하도록 수정 완료 --- packages/ui/src/core/DraggableNodeWrapper.tsx | 49 --------- packages/ui/src/core/EditorNodeWrapper.tsx | 103 ++++++++++++++++++ packages/ui/src/types/nodeAction.ts | 2 +- packages/ui/src/types/rnd.ts | 15 +++ 4 files changed, 119 insertions(+), 50 deletions(-) delete mode 100644 packages/ui/src/core/DraggableNodeWrapper.tsx create mode 100644 packages/ui/src/core/EditorNodeWrapper.tsx create mode 100644 packages/ui/src/types/rnd.ts diff --git a/packages/ui/src/core/DraggableNodeWrapper.tsx b/packages/ui/src/core/DraggableNodeWrapper.tsx deleted file mode 100644 index b896ee4..0000000 --- a/packages/ui/src/core/DraggableNodeWrapper.tsx +++ /dev/null @@ -1,49 +0,0 @@ -//에디터 모드전용 노드 렌더러 래퍼 컴포넌트 -import { BaseNode } from "types"; - -interface WrapperProps { - children: React.ReactNode; - node: BaseNode; - isSelected: boolean; - onDragStart: (e: React.MouseEvent, id: string) => void; -} - -export default function DraggableNodeWrapper({ - children, - node, - isSelected, - onDragStart, -}: WrapperProps) { - const wrapperStyle: React.CSSProperties = { - position: "absolute", // 핵심: 캔버스 내에서 자유 배치 - left: node["layout"].x, - top: node["layout"].y, - width: node["layout"].width, - height: node["layout"].height, - zIndex: node["layout"].zIndex, - - // 선택되었을 때 시각적 피드백 (테두리 등) - outline: isSelected ? "2px solid blue" : "none", - cursor: "move", - }; - - return ( -
onDragStart(e, node.id)} - className="" //TODO - 필요시 클래스네임 추가(dnd상황에서 스타일 강조 등) - > - {/* 실제 컴포넌트(Hero 등)는 이 안에 렌더링됨 */} - {children} - - {/* 리사이즈 핸들 등은 에디터 모드에서만 오버레이로 표시 */} - {/* TODO-추후 호버시 리사이즈 핸들 렌더링 하도록 수정 필요! */} - {isSelected && ( - <> -
- {/* TODO-기타 리사이즈 핸들들 렌더링은 이곳에서 ... */} - - )} -
- ); -} diff --git a/packages/ui/src/core/EditorNodeWrapper.tsx b/packages/ui/src/core/EditorNodeWrapper.tsx new file mode 100644 index 0000000..94df8c0 --- /dev/null +++ b/packages/ui/src/core/EditorNodeWrapper.tsx @@ -0,0 +1,103 @@ +//에디터 모드전용 노드 렌더러 래퍼 컴포넌트 +import clsx from "clsx"; +import { Rnd } from "react-rnd"; +import { BaseNode } from "types"; +import { CanvasState, Layer } from "types/rnd"; + +interface WrapperProps { + children: React.ReactNode; + node: BaseNode; + selectedId: string; + updateNode: (id: string, updates: Partial) => void; //노드의 레이아웃 업데이트 함수 from editor의 스토어 액션 + selectNode: (id: string) => void; + canvas: CanvasState; +} + +//에디터 전용 노드 렌더러 래퍼 +//래퍼 컴포넌트에서 rnd작업 발생할때 현재 액션이 일어나는 노드 id는 몰라도 될듯? -> 항상 스토어의 selectedId를 기준으로 데이터를 수정하면 된다. +export default function EditorNodeWrapper({ + children, + node, + selectedId, + updateNode, + selectNode, + canvas, +}: 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", + }; + + //TODO- 노드 선택 로직 구현, 선택 ID 공유하는 zustand 스토어 구현 필요 + + return ( + e.stopPropagation()} + //TODO-일단 이동중에 스토어 업데이트는 미루기 -> 성능 이슈 + // onDrag={(e, d) => updateNode(id, { x: d.x, y: d.y })} + onDragStop={(e, d) => updateNode(id, { x: d.x, y: d.y })} + onResizeStart={(e) => e.stopPropagation()} + //TODO-일단 리사이징중에 스토어 업데이트는 미루기 -> 성능 이슈 + /* + onResize={(e, dir, ref, delta, pos) => + updateNode(id, { + width: parseInt(ref.style.width), + height: parseInt(ref.style.height), + ...pos, + }) + } + */ + onResizeStop={(e, dir, ref, delta, pos) => + updateNode(id, { + width: parseInt(ref.style.width), + height: parseInt(ref.style.height), + ...pos, + }) + } + enableResizing={isSelected ? undefined : false} + disableDragging={!isSelected} + className={clsx("group cursor-pointer", isSelected && "z-50")} + resizeHandleClasses={{ + bottomLeft: isSelected + ? clsx(selectedNodeGuideClasses.handle, "-left-1.5 -bottom-1.5") + : undefined, + bottomRight: isSelected + ? clsx(selectedNodeGuideClasses.handle, "-right-1.5 -bottom-1.5") + : undefined, + topLeft: isSelected + ? clsx(selectedNodeGuideClasses.handle, "-left-1.5 -top-1.5") + : undefined, + topRight: isSelected + ? clsx(selectedNodeGuideClasses.handle, "-right-1.5 -top-1.5") + : undefined, + }} + > +
{ + e.stopPropagation(); + selectNode(id); + }} + style={wrapperStyle} + className={clsx( + "transition-shadow duration-200", + isSelected && selectedNodeGuideClasses.outline, + )} + > + {/* 실제 컴포넌트(Hero 등)는 이 안에 렌더링됨 */} + {children} +
+
+ ); +} diff --git a/packages/ui/src/types/nodeAction.ts b/packages/ui/src/types/nodeAction.ts index f8ed6e3..7fb6ada 100644 --- a/packages/ui/src/types/nodeAction.ts +++ b/packages/ui/src/types/nodeAction.ts @@ -3,7 +3,7 @@ export type ActionType = | "scroll" // 3. 스크롤 이동 | "alert" // 2. 알림창 | "modal" // 4. 모달 열기 - | "mutation"; // 1. 데이터 변경 (핵심!) + | "mutation"; // 1. 데이터 변경 (핵심!) -> 무시 export interface NodeAction { type: ActionType; diff --git a/packages/ui/src/types/rnd.ts b/packages/ui/src/types/rnd.ts new file mode 100644 index 0000000..19c3bdc --- /dev/null +++ b/packages/ui/src/types/rnd.ts @@ -0,0 +1,15 @@ +export interface Layer { + id: string; + x: number; + y: number; + width: number; + height: number; + fill: string; + content?: string; +} + +export interface CanvasState { + scale: number; + dx: number; + dy: number; +} From 72ac1a7c4b9f7bf81b1d0fa10eab63f216a9438d Mon Sep 17 00:00:00 2001 From: y-minion Date: Thu, 8 Jan 2026 11:25:45 +0900 Subject: [PATCH 6/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=20=EB=AA=A8=EB=93=9C=20=EB=85=B8=EB=93=9C=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=9F=AC=20=EB=9E=98=ED=8D=BC=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=88=98=EC=A0=95=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존에는 하나의 래퍼 컴포넌트 안에서 라이브, 에디터 모드를 분기 처리 하도록 했지만, 이렇게 되면 라이브 모드에서 사용하지않는 에디터 모드와 관련된 무거운 로직들을 갖고 있게된다. 그래서 각 상황에 따라 다른 래퍼 컴포넌트를 사용하도록 하기 위해 분리 했음. --- packages/ui/src/core/BaseLayoutWrapper.tsx | 1 + packages/ui/src/core/LiveModeWrapper.tsx | 21 --------------------- 2 files changed, 1 insertion(+), 21 deletions(-) delete mode 100644 packages/ui/src/core/LiveModeWrapper.tsx diff --git a/packages/ui/src/core/BaseLayoutWrapper.tsx b/packages/ui/src/core/BaseLayoutWrapper.tsx index 4946a1e..0f3c7ef 100644 --- a/packages/ui/src/core/BaseLayoutWrapper.tsx +++ b/packages/ui/src/core/BaseLayoutWrapper.tsx @@ -5,6 +5,7 @@ interface Props { node: BaseNode; } +//라이브 모드에서는 항상 사용, 에디터 모드에서는 상황에 따라 선택적으로 사용됩니다. export default function BaseLayoutNodeWrapper({ children, node }: Props) { const { x: top, y: left, width, height, zIndex } = node.layout; return ( diff --git a/packages/ui/src/core/LiveModeWrapper.tsx b/packages/ui/src/core/LiveModeWrapper.tsx deleted file mode 100644 index 038836b..0000000 --- a/packages/ui/src/core/LiveModeWrapper.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { BaseNode } from "types"; - -//라이브 모드에서 사용되는 노드 렌더러 래퍼 컴포넌트입니다. -interface props { - children: React.ReactNode; - node: BaseNode; -} - -export default function LiveModeWrapper({ children, node }: props) { - const { x, y, width, height, zIndex } = node.layout; - - const wrapperStyle: React.CSSProperties = { - position: "absolute", - left: x, - top: y, - width, - height, - zIndex, - }; - return
{children}
; -}