diff --git a/apps/editor/db.json b/apps/editor/db.json index 429b01b..307b5c4 100644 --- a/apps/editor/db.json +++ b/apps/editor/db.json @@ -1,56 +1,18 @@ { "nodes": [ - { - "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": "세상에 하나뿐인 당신의 웹사이트를 만들어보세요.", - "button": { "text": "시작하기", "link": "/signup" }, - "backgroundImage": { - "url": "https://images.example.com/hero-bg.jpg", - "alt": "추상적인 배경 이미지" - } - }, - "style": { - "layout": { - "display": "flex", - "flexDirection": "column", - "alignItems": "center", - "justifyContent": "center", - "gap": "20px" - }, - "dimensions": { "minHeight": "500px", "padding": "80px 24px" }, - "background": { - "backgroundColor": "#111827", - "backgroundSize": "cover", - "backgroundPosition": "center" - }, - "typography": { "color": "#FFFFFF", "textAlign": "center" }, - "className": "hero-container-main" - }, - "created_at": "2025-11-13T06:18:00Z" - }, { "id": "1002", "page_id": 201, "parent_id": null, "type": "Container", "position": 1, - "layout": { "x": 0, "y": 600, "width": 1440, "height": 800, "zIndex": 1 }, + "layout": { "x": 0, "y": 0, "width": 1440, "height": 800, "zIndex": 1 }, "props": {}, "style": { - "layout": { "display": "block" }, - "dimensions": { - "padding": "50px 24px", - "maxWidth": "1200px", - "margin": "0 auto" - }, - "background": { "backgroundColor": "#FFFFFF" }, + "display": "block", + "padding": "50px 24px", + "maxWidth": "1200px", + "backgroundColor": "#FFFFFF", "className": "content-section" }, "created_at": "2025-11-13T06:20:00Z" @@ -73,12 +35,10 @@ "level": "h2" }, "style": { - "typography": { - "color": "#111827", - "fontSize": "36px", - "textAlign": "center" - }, - "dimensions": { "marginBottom": "30px" } + "color": "#111827", + "fontSize": "36px", + "textAlign": "center", + "marginBottom": "30px" }, "created_at": "2025-11-13T06:21:00Z" }, @@ -96,16 +56,14 @@ "zIndex": 2 }, "props": { - "text": "직관적인 드래그 앤 드롭 인터페이스로 코딩 없이 웹사이트를 완성하세요. 모든 컴포넌트는 사용자가 원하는 대로 커스터마이징할 수 있습니다." + "text": "직관적인 드래그 앤 드롭 인터페이스로 코딩 없이 웹사이트를 완성하세요. 모든 컴포넌트는 사용자가 원하는 대로 커스터마이징할 수 있습니다.", + "level": "p" }, "style": { - "typography": { - "color": "#374151", - "fontSize": "18px", - "lineHeight": 1.6, - "textAlign": "center" - }, - "dimensions": { "maxWidth": "800px", "margin": "0 auto" } + "color": "#374151", + "fontSize": "18px", + "lineHeight": 1.6, + "textAlign": "center" }, "created_at": "2025-11-13T06:22:00Z" }, @@ -117,24 +75,19 @@ "position": 2, "layout": { "x": 0, - "y": 1400, + "y": 800, "width": 1440, "height": 600, "zIndex": 1 }, "props": { "id": "gallery" }, "style": { - "layout": { - "display": "grid", - "gridTemplateColumns": "1fr 1fr", - "gap": "20px" - }, - "dimensions": { - "padding": "50px 24px", - "maxWidth": "1200px", - "margin": "0 auto" - }, - "background": { "backgroundColor": "#F9FAFB" }, + "display": "grid", + "gridTemplateColumns": "1fr 1fr", + "gap": "20px", + "padding": "50px 24px", + "maxWidth": "1200px", + "backgroundColor": "#F9FAFB", "className": "gallery-section" }, "created_at": "2025-11-13T06:23:00Z" @@ -147,16 +100,14 @@ "position": 0, "layout": { "x": 50, "y": 50, "width": 600, "height": 300, "zIndex": 2 }, "props": { - "src": "data:image/jpeg;base64,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", + "src": "data:image/jpeg;base64,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", "alt": "갤러리 이미지 1" }, "style": { - "dimensions": { - "width": "100%", - "height": "300px", - "objectFit": "cover" - }, - "effects": { "borderRadius": "8px" } + "width": "100%", + "height": "300px", + "objectFit": "cover", + "borderRadius": "8px" }, "created_at": "2025-11-13T06:24:00Z" }, @@ -172,12 +123,10 @@ "alt": "갤러리 이미지 2" }, "style": { - "dimensions": { - "width": "100%", - "height": "300px", - "objectFit": "cover" - }, - "effects": { "borderRadius": "8px" } + "width": "100%", + "height": "300px", + "objectFit": "cover", + "borderRadius": "8px" }, "created_at": "2025-11-13T06:25:00Z" }, @@ -189,15 +138,14 @@ "position": 2, "layout": { "x": 50, "y": 360, "width": 600, "height": 50, "zIndex": 2 }, "props": { - "text": "이미지 설명 1" + "text": "이미지 설명 1", + "level": "p" }, "style": { - "typography": { - "textAlign": "center", - "fontSize": "14px", - "color": "#6B7280" - }, - "dimensions": { "marginTop": "10px" } + "textAlign": "center", + "fontSize": "14px", + "color": "#6B7280", + "marginTop": "10px" }, "created_at": "2025-11-13T06:26:00Z" }, @@ -209,15 +157,14 @@ "position": 3, "layout": { "x": 700, "y": 360, "width": 600, "height": 50, "zIndex": 2 }, "props": { - "text": "이미지 설명 2" + "text": "이미지 설명 2", + "level": "p" }, "style": { - "typography": { - "textAlign": "center", - "fontSize": "14px", - "color": "#6B7280" - }, - "dimensions": { "marginTop": "10px" } + "textAlign": "center", + "fontSize": "14px", + "color": "#6B7280", + "marginTop": "10px" }, "created_at": "2025-11-13T06:27:00Z" }, @@ -229,26 +176,26 @@ "position": 3, "layout": { "x": 600, - "y": 2100, + "y": 1500, "width": 200, "height": 60, "zIndex": 1 }, "props": { "text": "더 알아보기", - "link": "/features" + "action": { "type": "navigate", "url": "/features" } }, "style": { - "layout": { "display": "block", "margin": "40px auto" }, - "dimensions": { "padding": "15px 30px", "width": "200px" }, - "background": { "backgroundColor": "#3B82F6" }, - "typography": { - "color": "#FFFFFF", - "fontSize": "16px", - "fontWeight": "bold", - "textAlign": "center" - }, - "effects": { "borderRadius": "5px", "cursor": "pointer" } + "display": "block", + "padding": "15px 30px", + "width": "200px", + "backgroundColor": "#3B82F6", + "color": "#FFFFFF", + "fontSize": "16px", + "fontWeight": "bold", + "textAlign": "center", + "borderRadius": "5px", + "cursor": "pointer" }, "created_at": "2025-11-13T06:28:00Z" } diff --git a/apps/editor/src/components/editor/Canvas.tsx b/apps/editor/src/components/editor/Canvas.tsx index d675dfa..75604b1 100644 --- a/apps/editor/src/components/editor/Canvas.tsx +++ b/apps/editor/src/components/editor/Canvas.tsx @@ -2,6 +2,7 @@ import { useCanvas, + useClearNode, useCurNodes, useSelectedNodeId, useSelectNode, @@ -26,10 +27,17 @@ export default function Canvas() { const updateNode = useUpdateNodeLayout(); const canvasState = useCanvas(); const setCanvas = useSetCanvas(); + const clearNode = useClearNode(); const isPanning = useRef(false); const lastMousePos = useRef({ x: 0, y: 0 }); + function getParentNode(parentId: string | null): WcxNode | undefined { + if (!parentId) return undefined; + + return nodes?.find((n) => n.id === parentId); + } + //FIXME-각 노드들에 key속성 추가해주기. -> 리액트 경고 발생 //FIXME-nodes가 비어있는 상황에서 에러발생. -> Base Condition에 Root가 들어간다.(Root는 단지 더미 노드일뿐 로직에 들어가면 안된다.) /** @@ -52,6 +60,7 @@ export default function Canvas() { return ( handleWheel({ canvas: canvasState, e, setCanvas })} onMouseDown={(e) => - handleMouseDown({ e, isPanning, lastMousePos, selectNode }) + handleMouseDown({ e, isPanning, lastMousePos, clearNode }) } onMouseMove={(e) => handleMouseMove({ e, isPanning, lastMousePos, setCanvas, canvasState }) diff --git a/apps/editor/src/shared/lib/component-defaults.ts b/apps/editor/src/shared/lib/component-defaults.ts index 4e3f593..a2bce1f 100644 --- a/apps/editor/src/shared/lib/component-defaults.ts +++ b/apps/editor/src/shared/lib/component-defaults.ts @@ -1,4 +1,3 @@ - import { WcxNode } from "@repo/ui/types/nodes"; import { NodeStyle } from "@repo/ui/types/styles"; @@ -6,51 +5,18 @@ import { NodeStyle } from "@repo/ui/types/styles"; // 추천 코드를 반영하여 layout 필드를 분리하고 타입 안정성을 강화했습니다. export interface ComponentDefaults { props: Record; // 각 노드 타입에 맞는 props (통합 노드 타입에서 추론) - style: NodeStyle; // @repo/ui의 규격화된 스타일 구조 (root 등) + style: NodeStyle; // @repo/ui의 규격화된 스타일 구조 (평탄화된 구조) layout: WcxNode['layout']; // x, y, width, height, zIndex } export const COMPONENT_DEFAULTS: Record = { - Hero: { - props: { - heading: "Hero Heading", - subHeading: "Hero SubHeading", - image: { - url: "https://via.placeholder.com/800x400", - }, - button: { - text: "Action", - link: "#", - }, - }, - style: { - root: { - layout: { - display: "flex", - flexDirection: "column", - alignItems: "center", - justifyContent: "center", - }, - background: { backgroundColor: "#f0f0f0" }, - }, - }, - layout: { - x: 0, - y: 0, - width: 1000, // WcxNode layout 타입에 맞춰 숫자로 지정 - height: 400, - zIndex: 0, - }, - }, Image: { props: { src: "https://via.placeholder.com/400x300", alt: "Image", caption: "Image Caption", }, - style: { - root: {}, - }, + style: {}, layout: { x: 0, y: 0, @@ -65,13 +31,9 @@ export const COMPONENT_DEFAULTS: Record = { level: "h2", }, style: { - root: { - typography: { - color: "#000000", - fontSize: "24px", - fontWeight: "bold", - }, - }, + color: "#000000", + fontSize: "24px", + fontWeight: "bold", }, layout: { x: 0, @@ -87,12 +49,8 @@ export const COMPONENT_DEFAULTS: Record = { level: "h5", }, style: { - root: { - typography: { - color: "#333333", - fontSize: "16px", - }, - }, + color: "#333333", + fontSize: "16px", }, layout: { x: 0, @@ -107,11 +65,12 @@ export const COMPONENT_DEFAULTS: Record = { text: "Button", }, style: { - root: { - background: { backgroundColor: "#007bff" }, - typography: { color: "#ffffff" }, - effects: { borderRadius: "4px" }, - }, + backgroundColor: "#007bff", + color: "#ffffff", + borderRadius: "4px", + display: "flex", + alignItems: "center", + justifyContent: "center", }, layout: { x: 0, @@ -126,9 +85,8 @@ export const COMPONENT_DEFAULTS: Record = { tagName: "div", }, style: { - root: { - effects: { border: "1px dashed #ccc" }, - }, + border: "1px dashed #ccc", + backgroundColor: "#ffffff", }, layout: { x: 0, @@ -146,10 +104,9 @@ export const COMPONENT_DEFAULTS: Record = { closeOnOverlayClick: true, }, style: { - root: { - background: { backgroundColor: "#ffffff" }, - effects: { borderRadius: "8px" }, - }, + backgroundColor: "#ffffff", + borderRadius: "8px", + boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)", }, layout: { x: 0, @@ -159,4 +116,36 @@ export const COMPONENT_DEFAULTS: Record = { zIndex: 100, }, }, + // Stack 추가 + Stack: { + props: {}, + style: { + display: "flex", + flexDirection: "column", + gap: "10px", + padding: "20px", + backgroundColor: "#f9fafb", + border: "1px solid #e5e7eb", + }, + layout: { + x: 0, + y: 0, + width: 300, + height: 300, + zIndex: 0, + }, + }, + // Group 추가 + Group: { + props: {}, + style: {}, + layout: { + x: 0, + y: 0, + width: 200, + height: 200, + zIndex: 0, + }, + }, }; + diff --git a/apps/editor/src/stores/useEditorStore.ts b/apps/editor/src/stores/useEditorStore.ts index 1b4c1e6..848d58c 100644 --- a/apps/editor/src/stores/useEditorStore.ts +++ b/apps/editor/src/stores/useEditorStore.ts @@ -9,9 +9,9 @@ const useEditorStore = create( immer( combine( { - selectedNodeId: null as string | null, nodes: null as null | WcxNode[], //TODO- 추후에 현재 페이지에 해당하는 노드들을 받아오는 로직을 통해 해당 상태가 업데이트 되야 한다. -> EditorStoreInitializer컴포넌트에서 담당 canvas: { dx: 0, dy: 0, scale: 1 }, + selectedDepthPath: [] as string[], }, (set, get) => ({ setNode(nodes: WcxNode[]) { @@ -57,19 +57,41 @@ const useEditorStore = create( return res; //TODO- 재귀 삭제 함수로 추출된 노드id는 deleteNodes에 담겨 있다. 이 데이터를 바탕으로 DB수정 시도 }, - selectNode(id: string | null) { - set( - (state) => { - state.selectedNodeId = id; - }, - false, - "editorStore/selectNode", - ); + + //FIXME-🐛 버그 발견! -> 하위 노드에서 다른 가지로 넘어갈때 다시 상위 노드가 선택되는 버그 발견. 같은 계층의 자식 노드로 가지를 옮기려면 바로 선택될 수 있어야한다. + selectNode(targetNodeId: string) { + const path = get().selectedDepthPath; + const nodes = get().nodes; + if (!nodes) return; + + while (true) { + const targetNode = nodes.find((node) => node.id === targetNodeId); + if (!targetNode) return; + const parentNodeId = targetNode.parent_id; + + if (parentNodeId === null) { + set((state) => { + state.selectedDepthPath = [targetNodeId]; + }); + break; + } + + const parentPos = path.indexOf(parentNodeId); + + if (parentPos !== -1) { + set((state) => { + state.selectedDepthPath.splice(parentPos + 1); + state.selectedDepthPath.push(targetNodeId); + }); + break; + } + targetNodeId = parentNodeId; + } }, clearNode() { set( (state) => { - state.selectedNodeId = null; + state.selectedDepthPath = []; }, false, "editorStore/clearNode", @@ -125,6 +147,30 @@ const useEditorStore = create( state.canvas = { ...state.canvas, ...updates }; }); }, + + //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"; + }), }), ), ), @@ -165,7 +211,10 @@ export const useDeleteNode = () => useEditorStore((store) => store.deleteNode); * 선택된 노드가 없으면 null을 반환합니다. */ export const useSelectedNodeId = () => - useEditorStore((store) => store.selectedNodeId); + useEditorStore((store) => { + const path = store.selectedDepthPath; + return path.length > 0 ? path[path.length - 1] : null; + }); /** * [Action] 특정 노드를 선택(포커스)하는 함수를 반환합니다. diff --git a/apps/editor/src/utils/editor/canvasMouseHandler.ts b/apps/editor/src/utils/editor/canvasMouseHandler.ts index 91fb132..5105559 100644 --- a/apps/editor/src/utils/editor/canvasMouseHandler.ts +++ b/apps/editor/src/utils/editor/canvasMouseHandler.ts @@ -9,7 +9,7 @@ interface handleMouseDown { e: React.MouseEvent; isPanning: IsPanning; lastMousePos: LastMousePos; - selectNode: (id: string | null) => void; + clearNode: () => void; } interface handleMouseMove { @@ -28,10 +28,17 @@ export function handleMouseDown({ e, isPanning, lastMousePos, - selectNode, + clearNode, }: handleMouseDown) { if (e.button === 0) { - selectNode(null); + const target = e.target as HTMLElement; + const componentElement = target.closest( + "[data-component-type]", + ); + if (!componentElement) return; + if (componentElement.dataset.componentType !== "canvas") return; + clearNode(); + return; } e.preventDefault(); diff --git a/packages/ui/src/components/Button.tsx b/packages/ui/src/components/Button.tsx index 5eeeb13..0ba6112 100644 --- a/packages/ui/src/components/Button.tsx +++ b/packages/ui/src/components/Button.tsx @@ -5,15 +5,12 @@ import processNodeStyles from "utils/processNodeStyles"; export default function ButtonComponent({ node, - props, - style, children, }: NodeComponentProps) { const { mode } = useBuilderMode(); - const { text, action } = props; + const { text, action } = node.props; - //스타일 변환(className은 제외, 오직 CSS속성만) - const nodeStyleObj = processNodeStyles(style); + const cssProps = processNodeStyles(node.style); //액션 함수 생성 const excuteAction = useActionHandler(action); @@ -33,12 +30,13 @@ export default function ButtonComponent({ return ( ); diff --git a/packages/ui/src/components/Container.tsx b/packages/ui/src/components/Container.tsx index 9ab54d8..384209b 100644 --- a/packages/ui/src/components/Container.tsx +++ b/packages/ui/src/components/Container.tsx @@ -1,19 +1,18 @@ import { ContainerNode, NodeComponentProps } from "types"; import processNodeStyles from "utils/processNodeStyles"; -export default function Container({ +export default function ContainerComponent({ node, - style, children, }: NodeComponentProps) { - //스타일 변환 - const nodeStyleObj = processNodeStyles(style); + const cssProps = processNodeStyles(node.style); return (
{/* Container는 자식이 있을 경우 렌더링 */} {children} diff --git a/packages/ui/src/components/Group.tsx b/packages/ui/src/components/Group.tsx new file mode 100644 index 0000000..c9513a8 --- /dev/null +++ b/packages/ui/src/components/Group.tsx @@ -0,0 +1,20 @@ +import { GroupNode, NodeComponentProps } from "types"; +import processNodeStyles from "utils/processNodeStyles"; + +// Group.tsx +export default function GroupComponent({ + children, + node, +}: NodeComponentProps) { + const cssProps = processNodeStyles(node.style); + return ( +
+ {children} +
+ ); +} diff --git a/packages/ui/src/components/Heading.tsx b/packages/ui/src/components/Heading.tsx index 37099d3..07ca899 100644 --- a/packages/ui/src/components/Heading.tsx +++ b/packages/ui/src/components/Heading.tsx @@ -4,24 +4,22 @@ import { HeadingNode } from "../types/nodes"; export default function HeadingComponent({ node, - props, - style, children, }: NodeComponentProps) { - const { text, level = "h2" } = props; - const nodeStyleObj = processNodeStyles(style); + const { text, level = "h2" } = node.props; + const cssProps = processNodeStyles(node.style); const Tag = level; return ( -
- {text} - {/* 컨테이너일 경우 이곳에 children이 렌더링 되야 한다. */} + {text} {children} -
+ ); } diff --git a/packages/ui/src/components/Hero.tsx b/packages/ui/src/components/Hero.tsx deleted file mode 100644 index 2f80e87..0000000 --- a/packages/ui/src/components/Hero.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useBuilderMode } from "context/builderMode"; -import Image from "next/image"; -import processNodeStyles from "../utils/processNodeStyles"; -import { HeroNode, NodeComponentProps } from "types"; - -export default function HeroComponent({ - node, - props, - style, - children, -}: NodeComponentProps) { - const { mode } = useBuilderMode(); //현재 모드 확인 - - //이 props의 내부 Key에 따라서 하위에 렌더링될 요소들이 결정된다. - const { heading, subHeading, button, image } = props; - - //TODO - 🚨 style이나 node가 변경될 때만 재연산되게 useMemo로 메모이제이션 사용해야할 필요가 있다. - const nodeStyleObj = processNodeStyles(style); - - /** - * - * image태그가 z-0에 깔려있고 컨텐츠를 감싸는 컨테이너 div가 image태그 위에 존재합니다. - */ - const handleLinkClick = (e: React.MouseEvent) => { - if (mode === "editor") e.preventDefault(); - }; - - return ( - //TODO - 노드들이 드래그 앤 드롭될때 위치가 자유롭게 변하게 할 수 있어야한다. -
- {/* 배경 이미지 영역 */} - {image?.url && ( -
- {image.alt -
-
- )} - - {/* --- 콘텐츠 영역 (z-index를 높여서 이미지 위에 표시) --- */} -
-

- {heading} -

- - {subHeading && ( -

- {subHeading} -

- )} - {button && ( - - {button?.text} - - )} - {children} -
-
- ); -} diff --git a/packages/ui/src/components/Image.tsx b/packages/ui/src/components/Image.tsx index bfeb36d..6f168a4 100644 --- a/packages/ui/src/components/Image.tsx +++ b/packages/ui/src/components/Image.tsx @@ -5,30 +5,23 @@ import processNodeStyles from "utils/processNodeStyles"; export default function ImageComponent({ node, - props, - style, children, }: NodeComponentProps) { - const { src, alt = "사용자의 이미지", caption } = props; - const nodeStyleObj = processNodeStyles(style); + const { src, alt = "사용자의 이미지", caption } = node.props; + const cssProps = processNodeStyles(node.style); return (
- {alt} + {alt} {caption &&
{caption}
} {children}
diff --git a/packages/ui/src/components/Modal.tsx b/packages/ui/src/components/Modal.tsx index 12217f5..0db3094 100644 --- a/packages/ui/src/components/Modal.tsx +++ b/packages/ui/src/components/Modal.tsx @@ -33,25 +33,27 @@ const animationVariants = { * * 해당 모달 렌더러를 에디터에서 보여주고 싶다면 기본 상태는 isOpen:flase 이므로 에디터에 한해서만 강제로 updateNodeState(id, { isOpen: true })를 호출해서 보여줘야합니다. */ -export default function Modal({ +export default function ModalComponent({ node, - style, children, //모달안에 들어갈 버튼, 텍스트 등이 children으로 올 수 있습니다. }: NodeComponentProps) { const curNodeId = node.id; const animationType = node.props.animation || "default"; - //스타일 변환 - const nodeStyleObj = processNodeStyles(style); + const cssProps = processNodeStyles(node.style); return ( e.stopPropagation()} - className={`${style.root?.className || ""} pointer-events-auto relative bg-white shadow-2xl`} - style={nodeStyleObj.root} - variants={animationVariants[animationType as keyof typeof animationVariants] || animationVariants.default} + className={`${node.style.className || ""} pointer-events-auto relative bg-white shadow-2xl`} + style={cssProps} + variants={ + animationVariants[animationType as keyof typeof animationVariants] || + animationVariants.default + } initial="hidden" animate="visible" exit="exit" diff --git a/packages/ui/src/components/Stack.tsx b/packages/ui/src/components/Stack.tsx new file mode 100644 index 0000000..b1e0f15 --- /dev/null +++ b/packages/ui/src/components/Stack.tsx @@ -0,0 +1,20 @@ +import { NodeComponentProps, StackNode } from "types"; +import processNodeStyles from "utils/processNodeStyles"; + +export default function StackComponent({ + node, + children, +}: NodeComponentProps) { + const cssProps = processNodeStyles(node.style); + + return ( +
+ {children} +
+ ); +} diff --git a/packages/ui/src/components/Text.tsx b/packages/ui/src/components/Text.tsx index b73d626..8ca9a55 100644 --- a/packages/ui/src/components/Text.tsx +++ b/packages/ui/src/components/Text.tsx @@ -3,24 +3,22 @@ import processNodeStyles from "utils/processNodeStyles"; export default function TextComponent({ node, - props, - style, children, }: NodeComponentProps) { - const { text, level = "h2" } = props; + const { text, level = "h2" } = node.props; const Tag = level; - const nodeStyleObj = processNodeStyles(style); + const cssProps = processNodeStyles(node.style); return ( -
- {text} - {/* TODO-만약 텍스트 컴포넌트가 사진같은 정적 파일도 렌더링 해야한다면? */} + {text} {children} -
+ ); } diff --git a/packages/ui/src/components/index.tsx b/packages/ui/src/components/index.tsx index d8e3743..a113bbc 100644 --- a/packages/ui/src/components/index.tsx +++ b/packages/ui/src/components/index.tsx @@ -1,4 +1,3 @@ -export * from "./Hero"; export * from "./Heading"; export * from "./Image"; export * from "./Button"; diff --git a/packages/ui/src/core/EditorNodeWrapper.tsx b/packages/ui/src/core/EditorNodeWrapper.tsx index b761d62..ac27061 100644 --- a/packages/ui/src/core/EditorNodeWrapper.tsx +++ b/packages/ui/src/core/EditorNodeWrapper.tsx @@ -1,12 +1,13 @@ //에디터 모드전용 노드 렌더러 래퍼 컴포넌트 import clsx from "clsx"; import { Rnd } from "react-rnd"; -import { BaseNode } from "types"; +import { WcxNode } from "types"; import { CanvasState, Layer } from "types/rnd"; interface WrapperProps { children: React.ReactNode; - node: BaseNode; + parentNode: WcxNode | undefined; + node: WcxNode; selectedId: string | null; updateNode: (id: string, updates: Partial) => void; //노드의 레이아웃 업데이트 함수 from editor의 스토어 액션 selectNode: (id: string) => void; @@ -17,13 +18,21 @@ interface WrapperProps { //래퍼 컴포넌트에서 rnd작업 발생할때 현재 액션이 일어나는 노드 id는 몰라도 될듯? -> 항상 스토어의 selectedId를 기준으로 데이터를 수정하면 된다. export default function EditorNodeWrapper({ children, + parentNode, node, selectedId, updateNode, selectNode, canvas, }: WrapperProps) { + const isStackItem = parentNode?.type === "Stack"; + const hasRelativePosition = + !node.style.position || node.style.position === "relative"; + + const isSwitchItems = isStackItem && hasRelativePosition; + const isSelected = selectedId === node.id; + const isGroup = node.type === "Group"; const wrapperStyle: React.CSSProperties = { cursor: "move", }; @@ -45,7 +54,13 @@ export default function EditorNodeWrapper({ onDragStart={(e) => 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 })} + onDragStop={(e, d) => { + if (isSwitchItems) { + //현재 놓인 Y위치에 따라서 노드의 순서 변경을 고려해야한다. + //TODO-Stack내부에서 Item 노드의 순서 변경 로직 실행 + } + updateNode(id, { x: d.x, y: d.y }); + }} onResizeStart={(e) => e.stopPropagation()} //TODO-일단 리사이징중에 스토어 업데이트는 미루기 -> 성능 이슈 /* @@ -64,7 +79,7 @@ export default function EditorNodeWrapper({ ...pos, }) } - enableResizing={isSelected ? undefined : false} + enableResizing={isGroup ? undefined : isSelected ? undefined : false} disableDragging={!isSelected} className={clsx("group cursor-pointer", isSelected && "z-50")} resizeHandleClasses={{ diff --git a/packages/ui/src/core/NodeRenderer.tsx b/packages/ui/src/core/NodeRenderer.tsx index 75a7d9c..1facb38 100644 --- a/packages/ui/src/core/NodeRenderer.tsx +++ b/packages/ui/src/core/NodeRenderer.tsx @@ -1,11 +1,12 @@ import ContainerComponent from "components/Container"; import ButtonComponent from "components/Button"; import HeadingComponent from "components/Heading"; -import HeroComponent from "components/Hero"; import ImageComponent from "components/Image"; -import Modal from "components/Modal"; import TextComponent from "components/Text"; import { WcxNode } from "types"; +import GroupComponent from "components/Group"; +import ModalComponent from "components/Modal"; +import StackComponent from "components/Stack"; export default function NodeRenderer({ node, @@ -15,70 +16,21 @@ export default function NodeRenderer({ children?: React.ReactNode; }) { switch (node.type) { - case "Hero": - return ( - - ); - case "Container": - return ( - - ); - + return ; case "Image": - return ( - - ); + return ; case "Heading": - return ( - - ); + return ; case "Text": - return ( - - ); + return ; case "Button": - return ( - - ); + return ; case "Modal": - return ( - - ); + return ; + case "Group": + return ; + case "Stack": + return ; } } diff --git a/packages/ui/src/types/component.ts b/packages/ui/src/types/component.ts index a984fb7..29132c3 100644 --- a/packages/ui/src/types/component.ts +++ b/packages/ui/src/types/component.ts @@ -3,7 +3,5 @@ import { WcxNode } from "./nodes"; //입력 받는 Node의 타입에 따라 달라진다. export interface NodeComponentProps { node: T; - props: T["props"]; - style: T["style"]; children?: React.ReactNode; } diff --git a/packages/ui/src/types/componentProps.ts b/packages/ui/src/types/componentProps.ts index 2bdf941..1e982d6 100644 --- a/packages/ui/src/types/componentProps.ts +++ b/packages/ui/src/types/componentProps.ts @@ -3,21 +3,7 @@ import { NodeAction } from "./nodeAction"; import { WcxNode } from "./nodes"; -// 1. Hero 컴포넌트 Props -export interface HeroProps { - heading: string; - subHeading?: string; - button?: { - text: string; - link: string; - }; - - //만약 사진이 없다면 null을 입력 해야합니다. - image?: { - url: string; - alt?: string; - }; -} +// HeroProps 제거됨 - 레거시 // 2. Image 컴포넌트 Props export interface ImageProps { src: string; diff --git a/packages/ui/src/types/nodes.ts b/packages/ui/src/types/nodes.ts index a67779c..04d977f 100644 --- a/packages/ui/src/types/nodes.ts +++ b/packages/ui/src/types/nodes.ts @@ -27,11 +27,6 @@ export interface BaseNode { //type에 따라 props가 동적으로 정해져서 모두 다르게 타입 선언 해야함. // 2. 각 노드별 구체적 정의 (type과 props를 묶음) -export interface HeroNode extends BaseNode { - type: "Hero"; - props: componentProps.HeroProps; -} - export interface ImageNode extends BaseNode { type: "Image"; props: componentProps.ImageProps; @@ -61,13 +56,26 @@ export interface ModalNode extends BaseNode { props: componentProps.ModalProps; } +export interface GroupNode extends BaseNode { + type: "Group"; + props: { + // props 없음 - 순수 논리적 그룹핑 + }; +} + +export interface StackNode extends BaseNode { + type: "Stack"; + props: {}; +} + // 3. 통합 노드 타입 // 이제 WcxNode 타입을 쓰면 type 체크 시 props가 자동 추론. export type WcxNode = - | HeroNode | ImageNode | HeadingNode | ButtonNode | ContainerNode | TextNode - | ModalNode; + | ModalNode + | GroupNode + | StackNode; diff --git a/packages/ui/src/types/styles.ts b/packages/ui/src/types/styles.ts index 173d883..b37e01e 100644 --- a/packages/ui/src/types/styles.ts +++ b/packages/ui/src/types/styles.ts @@ -1,35 +1,12 @@ import { CSSProperties } from "react"; -//노드의 스타일 타입은 반드시 카테고리 별로 구분되어 저장되야 한다. -//최소 단위. 하나의 요소에 적용될 스타일 그룹 - -type ElementStyleKey = - | `layout` //전체적인 레이아웃 - | "dimensions" //노드의 크기 - | "typography" //폰트 관련 CSS - | "background" //배경 - | "effects"; // boxShadow, opacity, borderRadius 등 -export interface ElementStyle - //FIXME - root 스타일은 크기(dimensions),레이아웃(layout)을 제외한 나머지(배경, 테두리, 패딩)만 담당하도록 수정해야한다. - extends Partial> { - // 2. HTML 클래스 (Tailwind 유틸리티 등) -> 추후 바이브 코딩의 결과물을 받았을때 사용될 속성입니다. +/** + * 노드 스타일 타입 (단순화) + * + * - 평탄한 CSS 객체 구조 + * - className을 추가 속성으로 포함 + * - 카테고리별 중첩 제거 + */ +export interface NodeStyle extends CSSProperties { className?: string; - - // 3. 확장성을 위한 인덱스 시그니처 - // 나중에 'animation'이나 'hover' 같은 그룹이 추가 예정 - [key: string]: CSSProperties | string | undefined; -} - -export type NodeStyleKey = - | "root" //필수: 최상위 컨테이너 -> 항상 최상위 컨테이너 스타일은 root로 지정 - | "button" // 옵션: 버튼 - | "heading" // 옵션: 제목 - | "subHeading" // 옵션: 부제목 - | "image" // 옵션: 이미지 - | "text"; // 옵션: 텍스트 - -// 2. 컴포넌트 전체 스타일: 부위별(Key)로 ElementStyle을 가짐 - -export interface NodeStyle extends Partial> { - [key: string]: ElementStyle | undefined; } diff --git a/packages/ui/src/utils/applyStyles.ts b/packages/ui/src/utils/applyStyles.ts deleted file mode 100644 index 0683ee2..0000000 --- a/packages/ui/src/utils/applyStyles.ts +++ /dev/null @@ -1,51 +0,0 @@ -//참고 자료->https://www.notion.so/Object-assign-2b175c9287fa8077b766de146b87a7e2?source=copy_link -//TODO-추후 성능 개선 필요(메모이제이션) -import { CSSProperties } from "react"; -import { ElementStyle } from "../types/styles"; - -/** - * - * @param styleData 노드의 style 데이터: JsonB형태 - * - * 카테고리별로 중첩되어 있는 스타일 데이터를 평탄화하여 인라인 문자열로 반환한다. - * 해당 반환값을 바로 style속성의 값으로 삽입할 수 있다. - * - */ -export default function applyStyles( - styleData: ElementStyle, -): CSSProperties | undefined { - if (!styleData) return; - - 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") { - const categoryStyles = styleData[key] as Record; - - for (const styleKey in categoryStyles) { - // 레이아웃 속성이면 건너뜀 (Wrapper가 담당) - if (LAYOUT_PROPERTIES.has(styleKey)) continue; - - combinedStyles[styleKey] = categoryStyles[styleKey]; - } - } - } - - return combinedStyles; -} diff --git a/packages/ui/src/utils/processNodeStyles.ts b/packages/ui/src/utils/processNodeStyles.ts index b252d2c..4451b60 100644 --- a/packages/ui/src/utils/processNodeStyles.ts +++ b/packages/ui/src/utils/processNodeStyles.ts @@ -1,23 +1,39 @@ import { CSSProperties } from "react"; -import { NodeStyle, NodeStyleKey } from "types"; -import applyStyles from "./applyStyles"; +import { NodeStyle } from "types"; /** + * 노드 스타일 처리 (간소화) * - * @param style 평탄화 되지 않은 노드의 스타일 객체 - * @returns 노드의 하위 요소들에 대한 스타일이 css객체로 변환된 객체 + * - className은 제외하고 CSS 속성만 반환 + * - 레이아웃 속성(width, height 등)은 EditorNodeWrapper가 담당하므로 필터링 * - * 상위 노드객체를 받아서 각 키(노드의 내부 요소들)에 해당하는 스타일을 적용하고 CSSProperties 객체로 변환하는 역할을 수행. - * 최종적으로 각 하위 요소별 스타일이 CSSProperties 객체로 매핑된 객체를 반환. - * 이때 하위 요소별 스타일객체는 평탄화 되어있습니다. + * @param style 노드의 style 객체 + * @returns CSS 속성만 포함된 객체 (className 제외, 레이아웃 속성 제외) */ -export default function processNodeStyles(style: NodeStyle) { - const nodeStyleObj: Partial> = {}; - Object.entries(style).forEach(([key, value]) => { - if (!value) return; - const styleKey = key as NodeStyleKey; - nodeStyleObj[styleKey] = applyStyles(value); - }); +export default function processNodeStyles(style: NodeStyle): CSSProperties { + // EditorNodeWrapper가 제어하는 레이아웃 속성 목록 + const LAYOUT_PROPERTIES = new Set([ + "width", + "height", + "position", + "top", + "bottom", + "left", + "right", + "zIndex", + "transform", + ]); - return nodeStyleObj; + // className 제거 + const { className, ...cssProps } = style; + + // 레이아웃 속성 필터링 + const result: Record = {}; + for (const [key, value] of Object.entries(cssProps)) { + if (!LAYOUT_PROPERTIES.has(key)) { + result[key] = value; + } + } + + return result as CSSProperties; }