Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 55 additions & 108 deletions apps/editor/db.json

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion apps/editor/src/components/editor/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import {
useCanvas,
useClearNode,
useCurNodes,
useSelectedNodeId,
useSelectNode,
Expand All @@ -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는 단지 더미 노드일뿐 로직에 들어가면 안된다.)
/**
Expand All @@ -52,6 +60,7 @@ export default function Canvas() {
return (
<EditorNodeWrapper
node={parentNode}
parentNode={getParentNode(parentNode.parent_id)}
selectedId={selectedNodeId}
updateNode={updateNode}
selectNode={selectNode}
Expand All @@ -77,6 +86,7 @@ export default function Canvas() {
return (
<EditorNodeWrapper
node={parentNode}
parentNode={getParentNode(parentNode.parent_id)}
selectedId={selectedNodeId}
updateNode={updateNode}
selectNode={selectNode}
Expand All @@ -89,10 +99,11 @@ export default function Canvas() {

return (
<div
data-component-type="canvas"
className="relative h-full w-full flex-1 cursor-grab overflow-hidden bg-white active:cursor-grabbing"
onWheel={(e) => 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 })
Expand Down
111 changes: 50 additions & 61 deletions apps/editor/src/shared/lib/component-defaults.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,22 @@

import { WcxNode } from "@repo/ui/types/nodes";
import { NodeStyle } from "@repo/ui/types/styles";

// ComponentDefault Data Type
// 추천 코드를 반영하여 layout 필드를 분리하고 타입 안정성을 강화했습니다.
export interface ComponentDefaults {
props: Record<string, unknown>; // 각 노드 타입에 맞는 props (통합 노드 타입에서 추론)
style: NodeStyle; // @repo/ui의 규격화된 스타일 구조 (root 등)
style: NodeStyle; // @repo/ui의 규격화된 스타일 구조 (평탄화된 구조)
layout: WcxNode['layout']; // x, y, width, height, zIndex
}

export const COMPONENT_DEFAULTS: Record<WcxNode['type'], ComponentDefaults> = {
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,
Expand All @@ -65,13 +31,9 @@ export const COMPONENT_DEFAULTS: Record<WcxNode['type'], ComponentDefaults> = {
level: "h2",
},
style: {
root: {
typography: {
color: "#000000",
fontSize: "24px",
fontWeight: "bold",
},
},
color: "#000000",
fontSize: "24px",
fontWeight: "bold",
},
layout: {
x: 0,
Expand All @@ -87,12 +49,8 @@ export const COMPONENT_DEFAULTS: Record<WcxNode['type'], ComponentDefaults> = {
level: "h5",
},
style: {
root: {
typography: {
color: "#333333",
fontSize: "16px",
},
},
color: "#333333",
fontSize: "16px",
},
layout: {
x: 0,
Expand All @@ -107,11 +65,12 @@ export const COMPONENT_DEFAULTS: Record<WcxNode['type'], ComponentDefaults> = {
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,
Expand All @@ -126,9 +85,8 @@ export const COMPONENT_DEFAULTS: Record<WcxNode['type'], ComponentDefaults> = {
tagName: "div",
},
style: {
root: {
effects: { border: "1px dashed #ccc" },
},
border: "1px dashed #ccc",
backgroundColor: "#ffffff",
},
layout: {
x: 0,
Expand All @@ -146,10 +104,9 @@ export const COMPONENT_DEFAULTS: Record<WcxNode['type'], ComponentDefaults> = {
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,
Expand All @@ -159,4 +116,36 @@ export const COMPONENT_DEFAULTS: Record<WcxNode['type'], ComponentDefaults> = {
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,
},
},
};

71 changes: 60 additions & 11 deletions apps/editor/src/stores/useEditorStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]) {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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";
}),
}),
),
),
Expand Down Expand Up @@ -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] 특정 노드를 선택(포커스)하는 함수를 반환합니다.
Expand Down
13 changes: 10 additions & 3 deletions apps/editor/src/utils/editor/canvasMouseHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface handleMouseDown {
e: React.MouseEvent;
isPanning: IsPanning;
lastMousePos: LastMousePos;
selectNode: (id: string | null) => void;
clearNode: () => void;
}

interface handleMouseMove {
Expand All @@ -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<HTMLElement>(
"[data-component-type]",
);
if (!componentElement) return;
if (componentElement.dataset.componentType !== "canvas") return;
clearNode();

return;
}
e.preventDefault();
Expand Down
16 changes: 7 additions & 9 deletions packages/ui/src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,12 @@ import processNodeStyles from "utils/processNodeStyles";

export default function ButtonComponent({
node,
props,
style,
children,
}: NodeComponentProps<ButtonNode>) {
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);
Expand All @@ -33,12 +30,13 @@ export default function ButtonComponent({
return (
<button
type="button"
data-component-type={node.type}
data-component-id={node.id}
style={nodeStyleObj.root}
className={`${style.root?.className} ${mode === "editor" ? "cursor-default" : "cursor-pointer"} flex h-full w-full items-center justify-center transition-all active:scale-95`}
onClick={clickHandler} //이벤트 연결
style={cssProps}
className={`${node.style.className || ""} ${mode === "editor" ? "cursor-default" : "cursor-pointer"} flex h-full w-full items-center justify-center transition-all active:scale-95`}
onClick={clickHandler}
>
<span style={nodeStyleObj.text}>{text}</span>
{text}
{children}
</button>
);
Expand Down
Loading