Skip to content

Commit 3f8ef9f

Browse files
authored
Merge pull request #13 from WebCreatorX/feature/node-component
📝 PR Title: 노드 렌더링 시스템 아키텍처 개편 및 에디터 래퍼 고도화
2 parents dc413bc + 72ac1a7 commit 3f8ef9f

File tree

16 files changed

+300
-96
lines changed

16 files changed

+300
-96
lines changed

.vscode/settings.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"editor.quickSuggestions": {
3+
"strings": true
4+
},
5+
"css.validate": false,
6+
"tailwindCSS.includeLanguages": {
7+
"typescript": "javascript",
8+
"typescriptreact": "javascript"
9+
},
10+
"files.associations": {
11+
"*.css": "tailwindcss"
12+
},
13+
"tailwindCSS.experimental.classRegex": [
14+
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
15+
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
16+
["clsx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
17+
[
18+
"resizeHandleClasses\\s*=\\s*\\{\\{([\\s\\S]*?)\\}\\}",
19+
"[\"'`]([^\"'`]*).*?[\"'`]"
20+
]
21+
],
22+
"todo-tree.tree.disableCompactFolders": true
23+
}

apps/editor/db.json

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
{
22
"nodes": [
33
{
4-
"id": 1001,
4+
"id": "1001",
55
"page_id": 201,
66
"parent_id": null,
77
"type": "Hero",
88
"position": 0,
9+
"layout": { "x": 0, "y": 0, "width": 1440, "height": 600, "zIndex": 1 },
910
"props": {
1011
"heading": "WebCreator-X에 오신 것을 환영합니다",
1112
"subheading": "세상에 하나뿐인 당신의 웹사이트를 만들어보세요.",
@@ -35,11 +36,12 @@
3536
"created_at": "2025-11-13T06:18:00Z"
3637
},
3738
{
38-
"id": 1002,
39+
"id": "1002",
3940
"page_id": 201,
4041
"parent_id": null,
4142
"type": "Container",
4243
"position": 1,
44+
"layout": { "x": 0, "y": 600, "width": 1440, "height": 800, "zIndex": 1 },
4345
"props": {},
4446
"style": {
4547
"layout": { "display": "block" },
@@ -54,11 +56,12 @@
5456
"created_at": "2025-11-13T06:20:00Z"
5557
},
5658
{
57-
"id": 1003,
59+
"id": "1003",
5860
"page_id": 201,
59-
"parent_id": 1002,
61+
"parent_id": "1002",
6062
"type": "Heading",
6163
"position": 0,
64+
"layout": { "x": 100, "y": 50, "width": 1200, "height": 100, "zIndex": 2 },
6265
"props": {
6366
"text": "주요 기능 소개",
6467
"level": "h2"
@@ -74,11 +77,12 @@
7477
"created_at": "2025-11-13T06:21:00Z"
7578
},
7679
{
77-
"id": 1004,
80+
"id": "1004",
7881
"page_id": 201,
79-
"parent_id": 1002,
82+
"parent_id": "1002",
8083
"type": "Text",
8184
"position": 1,
85+
"layout": { "x": 100, "y": 150, "width": 800, "height": 200, "zIndex": 2 },
8286
"props": {
8387
"text": "직관적인 드래그 앤 드롭 인터페이스로 코딩 없이 웹사이트를 완성하세요. 모든 컴포넌트는 사용자가 원하는 대로 커스터마이징할 수 있습니다."
8488
},
@@ -94,11 +98,12 @@
9498
"created_at": "2025-11-13T06:22:00Z"
9599
},
96100
{
97-
"id": 1005,
101+
"id": "1005",
98102
"page_id": 201,
99103
"parent_id": null,
100104
"type": "Container",
101105
"position": 2,
106+
"layout": { "x": 0, "y": 1400, "width": 1440, "height": 600, "zIndex": 1 },
102107
"props": { "id": "gallery" },
103108
"style": {
104109
"layout": {
@@ -117,11 +122,12 @@
117122
"created_at": "2025-11-13T06:23:00Z"
118123
},
119124
{
120-
"id": 1006,
125+
"id": "1006",
121126
"page_id": 201,
122-
"parent_id": 1005,
127+
"parent_id": "1005",
123128
"type": "Image",
124129
"position": 0,
130+
"layout": { "x": 50, "y": 50, "width": 600, "height": 300, "zIndex": 2 },
125131
"props": {
126132
"src": "https://images.example.com/gallery-1.jpg",
127133
"alt": "갤러리 이미지 1"
@@ -137,11 +143,12 @@
137143
"created_at": "2025-11-13T06:24:00Z"
138144
},
139145
{
140-
"id": 1007,
146+
"id": "1007",
141147
"page_id": 201,
142-
"parent_id": 1005,
148+
"parent_id": "1005",
143149
"type": "Image",
144150
"position": 1,
151+
"layout": { "x": 700, "y": 50, "width": 600, "height": 300, "zIndex": 2 },
145152
"props": {
146153
"src": "https://images.example.com/gallery-2.jpg",
147154
"alt": "갤러리 이미지 2"
@@ -157,11 +164,12 @@
157164
"created_at": "2025-11-13T06:25:00Z"
158165
},
159166
{
160-
"id": 1008,
167+
"id": "1008",
161168
"page_id": 201,
162-
"parent_id": 1005,
169+
"parent_id": "1005",
163170
"type": "Text",
164171
"position": 2,
172+
"layout": { "x": 50, "y": 360, "width": 600, "height": 50, "zIndex": 2 },
165173
"props": {
166174
"text": "이미지 설명 1"
167175
},
@@ -176,11 +184,12 @@
176184
"created_at": "2025-11-13T06:26:00Z"
177185
},
178186
{
179-
"id": 1009,
187+
"id": "1009",
180188
"page_id": 201,
181-
"parent_id": 1005,
189+
"parent_id": "1005",
182190
"type": "Text",
183191
"position": 3,
192+
"layout": { "x": 700, "y": 360, "width": 600, "height": 50, "zIndex": 2 },
184193
"props": {
185194
"text": "이미지 설명 2"
186195
},
@@ -195,11 +204,12 @@
195204
"created_at": "2025-11-13T06:27:00Z"
196205
},
197206
{
198-
"id": 1010,
207+
"id": "1010",
199208
"page_id": 201,
200209
"parent_id": null,
201210
"type": "Button",
202211
"position": 3,
212+
"layout": { "x": 600, "y": 2100, "width": 200, "height": 60, "zIndex": 1 },
203213
"props": {
204214
"text": "더 알아보기",
205215
"link": "/features"

apps/editor/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"dependencies": {
1414
"@repo/ui": "workspace:*",
1515
"@tanstack/react-query": "^5.90.8",
16+
"immer": "^11.1.3",
1617
"json-server": "^1.0.0-beta.3",
1718
"next": "15.5.9",
1819
"react": "19.1.0",

apps/editor/src/styles.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import "tailwindcss";

packages/ui/package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"name": "@repo/ui",
33
"version": "0.0.0",
44
"exports": {
5-
"./renderer": "./src/renderer/NodeRenderer.tsx"
5+
"./renderer": "./src/renderer/NodeRenderer.tsx",
6+
"./*": "./src/*"
67
},
78
"scripts": {
89
"type-check": "tsc --noEmit"
@@ -12,12 +13,17 @@
1213
"react": "19.1.0"
1314
},
1415
"devDependencies": {
16+
"@tailwindcss/postcss": "^4",
1517
"@types/node": "^20",
1618
"@types/react": "^19",
1719
"@types/react-dom": "^19",
20+
"postcss": "^8.5.6",
21+
"tailwindcss": "^4",
1822
"typescript": "^5"
1923
},
2024
"dependencies": {
21-
"framer-motion": "^12.23.26"
25+
"clsx": "^2.1.1",
26+
"framer-motion": "^12.23.26",
27+
"react-rnd": "^10.5.2"
2228
}
2329
}

packages/ui/postcss.config.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const config = {
2+
plugins: ["@tailwindcss/postcss"],
3+
};
4+
5+
export default config;

packages/ui/src/core/BaseLayoutWrapper.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ interface Props {
55
node: BaseNode;
66
}
77

8+
//라이브 모드에서는 항상 사용, 에디터 모드에서는 상황에 따라 선택적으로 사용됩니다.
89
export default function BaseLayoutNodeWrapper({ children, node }: Props) {
910
const { x: top, y: left, width, height, zIndex } = node.layout;
1011
return (

packages/ui/src/core/DraggableNodeWrapper.tsx

Lines changed: 0 additions & 49 deletions
This file was deleted.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
//에디터 모드전용 노드 렌더러 래퍼 컴포넌트
2+
import clsx from "clsx";
3+
import { Rnd } from "react-rnd";
4+
import { BaseNode } from "types";
5+
import { CanvasState, Layer } from "types/rnd";
6+
7+
interface WrapperProps {
8+
children: React.ReactNode;
9+
node: BaseNode;
10+
selectedId: string;
11+
updateNode: (id: string, updates: Partial<Layer>) => void; //노드의 레이아웃 업데이트 함수 from editor의 스토어 액션
12+
selectNode: (id: string) => void;
13+
canvas: CanvasState;
14+
}
15+
16+
//에디터 전용 노드 렌더러 래퍼
17+
//래퍼 컴포넌트에서 rnd작업 발생할때 현재 액션이 일어나는 노드 id는 몰라도 될듯? -> 항상 스토어의 selectedId를 기준으로 데이터를 수정하면 된다.
18+
export default function EditorNodeWrapper({
19+
children,
20+
node,
21+
selectedId,
22+
updateNode,
23+
selectNode,
24+
canvas,
25+
}: WrapperProps) {
26+
const isSelected = selectedId === node.id;
27+
const wrapperStyle: React.CSSProperties = {
28+
// 선택되었을 때 시각적 피드백 (테두리 등)
29+
outline: isSelected ? "outline outline-gray-500 outline-2" : "none",
30+
cursor: "move",
31+
};
32+
33+
const { id } = node;
34+
const { width, height, x, y } = node.layout;
35+
const selectedNodeGuideClasses = {
36+
handle: "bg-white border rounded-full border-gray-500 !w-3 !h-3",
37+
outline: "ring ring-2 ring-gray-500",
38+
};
39+
40+
//TODO- 노드 선택 로직 구현, 선택 ID 공유하는 zustand 스토어 구현 필요
41+
42+
return (
43+
<Rnd
44+
size={{ width, height }}
45+
position={{ x, y }}
46+
scale={canvas.scale}
47+
onDragStart={(e) => e.stopPropagation()}
48+
//TODO-일단 이동중에 스토어 업데이트는 미루기 -> 성능 이슈
49+
// onDrag={(e, d) => updateNode(id, { x: d.x, y: d.y })}
50+
onDragStop={(e, d) => updateNode(id, { x: d.x, y: d.y })}
51+
onResizeStart={(e) => e.stopPropagation()}
52+
//TODO-일단 리사이징중에 스토어 업데이트는 미루기 -> 성능 이슈
53+
/*
54+
onResize={(e, dir, ref, delta, pos) =>
55+
updateNode(id, {
56+
width: parseInt(ref.style.width),
57+
height: parseInt(ref.style.height),
58+
...pos,
59+
})
60+
}
61+
*/
62+
onResizeStop={(e, dir, ref, delta, pos) =>
63+
updateNode(id, {
64+
width: parseInt(ref.style.width),
65+
height: parseInt(ref.style.height),
66+
...pos,
67+
})
68+
}
69+
enableResizing={isSelected ? undefined : false}
70+
disableDragging={!isSelected}
71+
className={clsx("group cursor-pointer", isSelected && "z-50")}
72+
resizeHandleClasses={{
73+
bottomLeft: isSelected
74+
? clsx(selectedNodeGuideClasses.handle, "-left-1.5 -bottom-1.5")
75+
: undefined,
76+
bottomRight: isSelected
77+
? clsx(selectedNodeGuideClasses.handle, "-right-1.5 -bottom-1.5")
78+
: undefined,
79+
topLeft: isSelected
80+
? clsx(selectedNodeGuideClasses.handle, "-left-1.5 -top-1.5")
81+
: undefined,
82+
topRight: isSelected
83+
? clsx(selectedNodeGuideClasses.handle, "-right-1.5 -top-1.5")
84+
: undefined,
85+
}}
86+
>
87+
<div
88+
onClick={(e) => {
89+
e.stopPropagation();
90+
selectNode(id);
91+
}}
92+
style={wrapperStyle}
93+
className={clsx(
94+
"transition-shadow duration-200",
95+
isSelected && selectedNodeGuideClasses.outline,
96+
)}
97+
>
98+
{/* 실제 컴포넌트(Hero 등)는 이 안에 렌더링됨 */}
99+
{children}
100+
</div>
101+
</Rnd>
102+
);
103+
}

packages/ui/src/core/LiveModeWrapper.tsx

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)