Skip to content

Commit f259814

Browse files
authored
Merge pull request #15 from WebCreatorX/feat/canvas-rendering
feat: 캔버스 렌더링 및 에디터 스토어 핵심 기능 구현
2 parents 5289398 + 835b480 commit f259814

File tree

22 files changed

+471
-36
lines changed

22 files changed

+471
-36
lines changed

apps/editor/db.json

Lines changed: 30 additions & 6 deletions
Large diffs are not rendered by default.

apps/editor/next.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ import type { NextConfig } from "next";
33
const nextConfig: NextConfig = {
44
output: "standalone", // 배포에 필요한 파일만 모아줍니다
55
transpilePackages: ["@repo/ui"],
6+
images: {
7+
remotePatterns: [
8+
{
9+
protocol: "https",
10+
hostname: "**", // 개발 편의를 위해 모든 도메인 허용 (운영 배포시에는 특정 도메인으로 제한)
11+
},
12+
],
13+
},
614
};
715

816
export default nextConfig;

apps/editor/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"private": true,
55
"scripts": {
66
"dev": "next dev --turbopack",
7+
"db": "json-server db.json --port 4000",
78
"build": "next build --turbopack",
89
"start": "next start",
910
"lint": "next lint",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"use server";
2+
3+
import { WcxNode } from "types";
4+
5+
const BASE_URL = process.env.NEXT_PUBLIC_JSON_SERVER_URL;
6+
7+
export default async function getNodesFromDB(
8+
pageId: number,
9+
): Promise<WcxNode[]> {
10+
try {
11+
const res = await fetch(`${BASE_URL}/nodes?page_id=${pageId}`, {
12+
cache: "no-store", //FIXME-캐시 설정을 Tag기반으로 수정해야함!!
13+
});
14+
15+
if (!res.ok) {
16+
throw new Error(`Failed to fetch nodes from JSON-Server:${res.status}`);
17+
}
18+
19+
const data = await res.json();
20+
21+
return data as WcxNode[];
22+
} catch (error) {
23+
console.error("❌ Error fetching nodes:", error);
24+
return [];
25+
}
26+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import getNodesFromDB from "@/actions/editor/getNodesFromDB";
2+
import Canvas from "@/components/editor/Canvas";
3+
import EditorStoreInitializer from "@/components/editor/EditorStoreInitializer";
4+
import { RuntimeProvider } from "@repo/ui/context/runtimeContext";
5+
6+
export default async function EditorPage() {
7+
const pageId = 201; //목데이터입니다.
8+
//서버 액션 함수(nodes데이터 패칭함수)
9+
const nodes = await getNodesFromDB(pageId);
10+
11+
return (
12+
<div className="flex h-screen w-screen flex-col gap-10">
13+
<h1 className="text-3xl text-amber-700">에디터 페이지 입니다.</h1>
14+
<EditorStoreInitializer initialNodes={nodes}>
15+
{/* 다른 에디터 관련 컴포넌트들은 이곳에서 렌더링 됩니다!(사이드바,매니패스트 수정 컴포넌트 등등...) */}
16+
<RuntimeProvider>
17+
<div className="relative h-full w-full border-2">
18+
<Canvas />
19+
</div>
20+
</RuntimeProvider>
21+
</EditorStoreInitializer>
22+
</div>
23+
);
24+
}

apps/editor/src/app/layout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import "../styles.css";
12
import QueryProvider from "src/providers/queryProvider";
23

34
export default function RootLayout({

apps/editor/src/app/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export default function Home() {
55
<div>
66
<h1>환경합니다</h1>
77
<Link href="test_click_to_edit">테스트 환경</Link>
8+
<br />
9+
<Link href="/editor">에디터 페이지</Link>
810
</div>
911
);
1012
}

apps/editor/src/components/Canvas.tsx renamed to apps/editor/src/components/editor/Canvas.tsx

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,34 @@
1+
"use client";
2+
13
import {
24
useCanvas,
35
useCurNodes,
46
useSelectedNodeId,
57
useSelectNode,
6-
useUpdateNode,
8+
useSetCanvas,
9+
useUpdateNodeLayout,
710
} from "@/stores/useEditorStore";
8-
import EditorNodeWrapper from "@repo/ui/core/EditorNodeWrapper.jsx";
9-
import NodeRenderer from "@repo/ui/core/NodeRenderer.jsx";
10-
import { WcxNode } from "@repo/ui/types/nodes.js";
11-
import React from "react";
11+
import {
12+
handleMouseDown,
13+
handleMouseMove,
14+
handleMouseUp,
15+
} from "@/utils/editor/canvasMouseHandler";
16+
import handleWheel from "@/utils/editor/handleWheel";
17+
import EditorNodeWrapper from "@repo/ui/core/EditorNodeWrapper";
18+
import NodeRenderer from "@repo/ui/core/NodeRenderer";
19+
import { WcxNode } from "@repo/ui/types/nodes";
20+
import React, { useRef } from "react";
1221

1322
export default function Canvas() {
1423
const nodes = useCurNodes();
1524
const selectedNodeId = useSelectedNodeId();
1625
const selectNode = useSelectNode();
17-
const updateNode = useUpdateNode();
26+
const updateNode = useUpdateNodeLayout();
1827
const canvasState = useCanvas();
28+
const setCanvas = useSetCanvas();
29+
30+
const isPanning = useRef(false);
31+
const lastMousePos = useRef({ x: 0, y: 0 });
1932

2033
//FIXME-각 노드들에 key속성 추가해주기. -> 리액트 경고 발생
2134
//FIXME-nodes가 비어있는 상황에서 에러발생. -> Base Condition에 Root가 들어간다.(Root는 단지 더미 노드일뿐 로직에 들어가면 안된다.)
@@ -74,5 +87,34 @@ export default function Canvas() {
7487
);
7588
}
7689

77-
return <div className="canvas-root relative">{renderTree({ id: null })}</div>;
90+
return (
91+
<div
92+
className="relative h-full w-full flex-1 cursor-grab overflow-hidden bg-white active:cursor-grabbing"
93+
onWheel={(e) => handleWheel({ canvas: canvasState, e, setCanvas })}
94+
onMouseDown={(e) =>
95+
handleMouseDown({ e, isPanning, lastMousePos, selectNode })
96+
}
97+
onMouseMove={(e) =>
98+
handleMouseMove({ e, isPanning, lastMousePos, setCanvas, canvasState })
99+
}
100+
onMouseUp={() => handleMouseUp({ isPanning })}
101+
onMouseLeave={() => handleMouseUp({ isPanning })}
102+
>
103+
<div
104+
style={{
105+
transform: `translate(${canvasState.dx}px, ${canvasState.dy}px) scale(${canvasState.scale})`,
106+
transformOrigin: "0 0",
107+
width: "100%",
108+
height: "100%",
109+
}}
110+
className="relative h-full w-full"
111+
>
112+
{/* 배경 격자 (Helper Grid) */}
113+
<div className="bg-grid-pattern pointer-events-none absolute inset-[-1000%] z-0 h-[3000%] w-[3000%]" />
114+
<div className="relative z-10 h-full w-full">
115+
{renderTree({ id: null })}
116+
</div>
117+
</div>
118+
</div>
119+
);
78120
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"use client";
2+
3+
import { useSetNode } from "@/stores/useEditorStore";
4+
import { useEffect, useRef } from "react";
5+
import { WcxNode } from "types";
6+
7+
interface EditorStoreInitializer {
8+
children: React.ReactNode;
9+
initialNodes: WcxNode[];
10+
}
11+
12+
//TODO-일단 DB와 연동하지 말자. 서버액션의 사용이유는 초기 렌더링 속도 향상임. 이후에는 클라이언트에서 스토어 업데이트를 할 예정.
13+
export default function EditorStoreInitializer({
14+
children,
15+
initialNodes,
16+
}: EditorStoreInitializer) {
17+
const initialized = useRef(false);
18+
const setNode = useSetNode();
19+
useEffect(() => {
20+
if (initialized.current) return;
21+
setNode(initialNodes);
22+
initialized.current = true;
23+
}, [initialNodes, setNode]);
24+
25+
return <>{children}</>;
26+
}

apps/editor/src/components/__tests__/Canvas.test.tsx renamed to apps/editor/src/components/editor/__tests__/Canvas.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
66
const mockUseCurNodes = vi.fn();
77
const mockUseSelectedNodeId = vi.fn();
88
const mockUseSelectNode = vi.fn();
9-
const mockUseUpdateNode = vi.fn();
9+
const mockUseUpdateNodeLayout = vi.fn();
1010
const mockUseCanvas = vi.fn();
1111
const mockUseSetCanvas = vi.fn();
1212

@@ -15,7 +15,7 @@ vi.mock("@/stores/useEditorStore", () => ({
1515
useCurNodes: () => mockUseCurNodes(), //지연 실행 -> 이렇게 되면 다른 테스트에서 목함수의 반환값을 바꿔도 언제나 새롭게 해당 함수가 호출 되므로 다른 테스트의 영향을 받지 않는다.
1616
useSelectedNodeId: () => mockUseSelectedNodeId(),
1717
useSelectNode: () => mockUseSelectNode(),
18-
useUpdateNode: () => mockUseUpdateNode(),
18+
useUpdateNodeLayout: () => mockUseUpdateNodeLayout(),
1919
useCanvas: () => mockUseCanvas(),
2020
useSetCanvas: () => mockUseSetCanvas(),
2121
}));

0 commit comments

Comments
 (0)