From f2b0d0d5ac73ab27c8c336a4c8284a5993ffd048 Mon Sep 17 00:00:00 2001 From: y-minion Date: Sun, 30 Nov 2025 16:49:46 +0900 Subject: [PATCH 01/22] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20=EB=85=B8?= =?UTF-8?q?=EB=93=9C=EC=9D=98=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존에는 노드의 최상위의 스타일만 정의하는 구조였는데, 이제는 노드 내부에 여러개로 이루어진 하위 요소들의 스타일도 구조화해서 정의할 수 있도록 수정완료. --- packages/ui/src/types/styles.ts | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/types/styles.ts b/packages/ui/src/types/styles.ts index 682f53e..2951230 100644 --- a/packages/ui/src/types/styles.ts +++ b/packages/ui/src/types/styles.ts @@ -1,13 +1,15 @@ import { CSSProperties } from "react"; //노드의 스타일 타입은 반드시 카테고리 별로 구분되어 저장되야 한다. -export interface NodeStyle { - layout?: CSSProperties; //전체적인 레이아웃 - dimensions?: CSSProperties; //노드의 크기 - typography?: CSSProperties; //폰트 관련 CSS - background?: CSSProperties; //배경 - effects?: CSSProperties; // boxShadow, opacity, borderRadius 등 +//최소 단위. 하나의 요소에 적용될 스타일 그룹 +type ElementStyleKey = + | `layout` //전체적인 레이아웃 + | "dimensions" //노드의 크기 + | "typography" //폰트 관련 CSS + | "background" //배경 + | "effects"; // boxShadow, opacity, borderRadius 등 +export interface ElementStyle extends Record { // 2. HTML 클래스 (Tailwind 유틸리티 등) -> 추후 바이브 코딩의 결과물을 받았을때 사용될 속성입니다. className?: string; @@ -15,3 +17,15 @@ export interface NodeStyle { // 나중에 'animation'이나 'hover' 같은 그룹이 추가 예정 [key: string]: CSSProperties | string | undefined; } + +type NodeStyleKey = + | "root" //필수: 최상위 컨테이너 + | "button" // 옵션: 제목 + | "heading" // 옵션: 부제목 + | "subHeading" // 옵션: 버튼 + | "image"; // 옵션: 이미지 + +// 컴포넌트 전체 스타일: 부위별(Key)로 ElementStyle을 가짐 +export interface NodeStyle extends Record { + [key: string]: ElementStyle | undefined; +} From 78631807bcafcf844cf40552bbe5788c42b819a5 Mon Sep 17 00:00:00 2001 From: y-minion Date: Sun, 30 Nov 2025 18:56:34 +0900 Subject: [PATCH 02/22] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95,=20?= =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=9D=B8=ED=84=B0=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20partial=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존의 스타일 인터페이스에서 해당하는 Key들을 모두 포함해야 했었는데, partial로 선택적으로 받도록 수정 완료 --- packages/ui/src/types/componentProps.ts | 4 +++- packages/ui/src/types/styles.ts | 10 ++++++---- packages/ui/src/utils/applyStyles.ts | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/types/componentProps.ts b/packages/ui/src/types/componentProps.ts index c775e8e..fbe212f 100644 --- a/packages/ui/src/types/componentProps.ts +++ b/packages/ui/src/types/componentProps.ts @@ -8,7 +8,9 @@ export interface HeroProps { text: string; link: string; }; - backgroundImage?: { + + //만약 사진이 없다면 null을 입력 해야합니다. + image?: { url: string; alt?: string; }; diff --git a/packages/ui/src/types/styles.ts b/packages/ui/src/types/styles.ts index 2951230..8a2198c 100644 --- a/packages/ui/src/types/styles.ts +++ b/packages/ui/src/types/styles.ts @@ -9,7 +9,8 @@ type ElementStyleKey = | "typography" //폰트 관련 CSS | "background" //배경 | "effects"; // boxShadow, opacity, borderRadius 등 -export interface ElementStyle extends Record { +export interface ElementStyle + extends Partial> { // 2. HTML 클래스 (Tailwind 유틸리티 등) -> 추후 바이브 코딩의 결과물을 받았을때 사용될 속성입니다. className?: string; @@ -18,14 +19,15 @@ export interface ElementStyle extends Record { [key: string]: CSSProperties | string | undefined; } -type NodeStyleKey = +export type NodeStyleKey = | "root" //필수: 최상위 컨테이너 | "button" // 옵션: 제목 | "heading" // 옵션: 부제목 | "subHeading" // 옵션: 버튼 | "image"; // 옵션: 이미지 -// 컴포넌트 전체 스타일: 부위별(Key)로 ElementStyle을 가짐 -export interface NodeStyle extends Record { +// 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 index 7f750df..71e2683 100644 --- a/packages/ui/src/utils/applyStyles.ts +++ b/packages/ui/src/utils/applyStyles.ts @@ -1,7 +1,7 @@ //참고 자료->https://www.notion.so/Object-assign-2b175c9287fa8077b766de146b87a7e2?source=copy_link //추후 성능 개선 필요(메모이제이션) import { CSSProperties } from "react"; -import { NodeStyle } from "../types/styles"; +import { ElementStyle } from "../types/styles"; /** * @@ -12,7 +12,7 @@ import { NodeStyle } from "../types/styles"; * */ export default function applyStyles( - styleData: NodeStyle, + styleData: ElementStyle, ): CSSProperties | undefined { if (!styleData) return; From 79a2c705177e21d124c2f36c19f378472168c4ff Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 1 Dec 2025 14:42:19 +0900 Subject: [PATCH 03/22] =?UTF-8?q?=F0=9F=93=9D=20monorepo=EA=B0=80=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=EB=9D=BC=EC=9D=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 팀원들을위한 모노레포 가이드 작성 완료 --- monorepoGuide.md | 126 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 monorepoGuide.md diff --git a/monorepoGuide.md b/monorepoGuide.md new file mode 100644 index 0000000..1fc47b9 --- /dev/null +++ b/monorepoGuide.md @@ -0,0 +1,126 @@ +# WebCreatorX Monorepo 가이드라인 + +이 프로젝트는 확장성과 코드 재사용성을 높이기 위해 **Monorepo(모노레포)** 구조로 전환되었습니다. +**Turborepo**와 **pnpm workspaces**를 기반으로 구성되어 있으며, 여러 애플리케이션과 공유 패키지를 하나의 저장소에서 효율적으로 관리합니다. + +팀원들이 새로운 구조에 빠르게 적응할 수 있도록 가이드를 제공하겠습니다. + +--- + +## 🏗️ 기술 스택 및 도구 + +- **Monorepo Tool**: [Turborepo](https://turbo.build/) (빌드 시스템 및 태스크 러너) +- **Package Manager**: [pnpm](https://pnpm.io/) (Workspaces 기능 사용) +- **Framework**: Next.js 15 (App Router) +- **Language**: TypeScript + +--- + +## 📂 디렉토리 구조 (Directory Structure) + +기존 단일 레포지토리 구조와 달리, 프로젝트는 크게 `apps`와 `packages`로 나뉩니다. + +``` +web-creator-x/ +├── apps/ # ⭐️ 배포 가능한 애플리케이션들이 위치합니다. +│ ├── editor/ # 메인 에디터 애플리케이션 (Next.js) +│ └── user-template/ # 사용자 템플릿 관련 애플리케이션 +├── packages/ # 🛠️ 여러 앱에서 공유하는 라이브러리/패키지들이 위치합니다. +│ └── ui/ # 공통 UI 컴포넌트 (@repo/ui) +├── package.json # 루트 설정 (Workspaces 정의) +├── pnpm-workspace.yaml # pnpm 워크스페이스 설정 +└── turbo.json # Turborepo 파이프라인 설정 +``` + +### 💡 주요 변경 사항 (Migration Notes) + +| ⚠️ 기존 (Single Repo) | ♻️ 변경 후 (Monorepo) | 설명 | +| --------------------------------------- | ----------------------------------- | --------------------------------------------------------------------- | +| `/src` | `/apps/editor/src` | 메인 앱 코드는 `apps/editor`로 이동했습니다. | +| `/src/components` | `/packages/ui/src` | 재사용 가능한 컴포넌트는 `packages/ui`로 분리되었습니다. | +| `import { Button } from '@/components'` | `import { Button } from '@repo/ui'` | 공통 컴포넌트는 패키지명(`@repo/ui`)으로 import 합니다. | +| `npm run dev` | `pnpm dev` | 루트에서 실행하면 모든 앱을 동시에 실행하거나, 필터링하여 실행합니다. | + +--- + +## 🚀 시작하기 (Getting Started) + +### 1. 필수 요구사항 + +- Node.js (LTS 버전 권장) +- **pnpm** (필수): `npm install -g pnpm` + +### 2. 설치 + +프로젝트 루트에서 의존성을 설치합니다. 모든 앱과 패키지의 의존성이 한 번에 설치됩니다. + +```bash +pnpm install +``` + +### 3. 개발 서버 실행 + +루트에서 다음 명령어를 실행하면 `apps` 내의 모든 애플리케이션이 동시에 실행됩니다. + +```bash +pnpm dev +``` + +특정 앱만 실행하고 싶다면 `--filter` 옵션을 사용하세요. + +```bash +# editor 앱만 실행 +pnpm --filter editor dev +``` + +### 4. 빌드 (Build) + +전체 프로젝트를 빌드합니다. Turborepo의 캐싱 기능을 통해 변경된 부분만 빌드되어 속도가 빠릅니다. + +```bash +pnpm build +``` + +--- + +## 🛠️ 개발 워크플로우 (Development Workflow) + +### 패키지 추가하기 (Adding Dependencies) + +🚨 특정 앱이나 패키지에 라이브러리를 설치할 때는 `--filter`를 사용해야 합니다. + +**예시: `editor` 앱에 `axios` 설치** + +```bash +pnpm add axios --filter editor +``` + +**예시: `ui` 패키지에 `clsx` 설치** + +```bash +pnpm add clsx --filter @repo/ui +``` + +### 공통 컴포넌트 작업 (Working with Shared UI) + +`packages/ui`에 있는 컴포넌트를 수정하면, 이를 사용하는 모든 앱(`editor` 등)에 즉시 반영됩니다. +새로운 공통 컴포넌트를 만들 때는 다음 규칙을 따라주세요: + +1. `packages/ui/src/components`에 컴포넌트 파일 생성 +2. `packages/ui/package.json`의 `exports` 설정 확인 (필요 시) +3. 앱에서 `import { ... } from '@repo/ui'`로 사용 + +### 새로운 앱 추가하기 + +`apps/` 폴더 안에 새로운 Next.js 프로젝트 등을 생성하면 자동으로 워크스페이스에 포함됩니다. +`package.json`의 이름을 고유하게 설정해주세요. + +--- + +## ❓ 자주 묻는 질문 (FAQ) + +**Q. 왜 `npm` 대신 `pnpm`을 쓰나요?** +A. `pnpm`은 디스크 공간을 효율적으로 사용하고 설치 속도가 매우 빠릅니다. 또한 모노레포의 워크스페이스 기능을 완벽하게 지원합니다. + +**Q. `turbo.json`은 무엇인가요?** +A. `build`, `dev`, `lint` 등의 스크립트 실행 순서와 캐싱 정책을 정의하는 파일입니다. 예를 들어, `build` 작업은 의존성 패키지가 먼저 빌드되어야 함을 설정할 수 있습니다. From 39fbb8a6dc3b234005e65354c70c068cde2d9798 Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 1 Dec 2025 14:43:31 +0900 Subject: [PATCH 04/22] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20=EB=85=B8?= =?UTF-8?q?=EB=93=9C=EC=9D=98=20=EC=9C=84=EC=B9=98=20=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=EC=9D=84=20=EA=B3=A0=EB=A0=A4=ED=95=9C=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/src/types/nodes.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/types/nodes.ts b/packages/ui/src/types/nodes.ts index 2963d09..798abc8 100644 --- a/packages/ui/src/types/nodes.ts +++ b/packages/ui/src/types/nodes.ts @@ -5,13 +5,22 @@ import { NodeStyle } from "./styles"; //노드의 타입 별로 달라지는 node타입을 선언 // 1. 공통 필드 (모든 노드가 무조건 가지는 것) -interface BaseNode { +export interface BaseNode { id: string; //UUID page_id: string | number; // 소속 페이지 parent_id: string | null; // 부모 노드 position: number; // 정렬 순서 style: NodeStyle; // 스타일 (공통) created_at?: string; + + //wrapper에서 사용될 데이터 + layout: { + x: number; + y: number; + width: number; // 혹은 string ('100%') + height: number; // 혹은 string ('auto') + zIndex: number; + }; } //type에 따라 props가 동적으로 정해져서 모두 다르게 타입 선언 해야함. From 3ad36873ff00741d2bf3849ef83a395d68e0a4f5 Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 1 Dec 2025 14:48:41 +0900 Subject: [PATCH 05/22] =?UTF-8?q?=E2=9C=A8=EC=83=81=EC=9C=84=20=EB=85=B8?= =?UTF-8?q?=EB=93=9C=EC=9D=98=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98=ED=95=A0=EB=95=8C=20=EC=82=AC=EC=9A=A9=EB=90=98?= =?UTF-8?q?=EB=8A=94=20processNodeStyles=ED=95=A8=EC=88=98=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 상위 노드객체를 받아서 노드의 내부 요소들에 해당하는 스타일을 적용하고 CSSProperties 객체로 변환하는 역할을 수행한다. --- packages/ui/src/utils/style.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 packages/ui/src/utils/style.ts diff --git a/packages/ui/src/utils/style.ts b/packages/ui/src/utils/style.ts new file mode 100644 index 0000000..b252d2c --- /dev/null +++ b/packages/ui/src/utils/style.ts @@ -0,0 +1,23 @@ +import { CSSProperties } from "react"; +import { NodeStyle, NodeStyleKey } from "types"; +import applyStyles from "./applyStyles"; + +/** + * + * @param style 평탄화 되지 않은 노드의 스타일 객체 + * @returns 노드의 하위 요소들에 대한 스타일이 css객체로 변환된 객체 + * + * 상위 노드객체를 받아서 각 키(노드의 내부 요소들)에 해당하는 스타일을 적용하고 CSSProperties 객체로 변환하는 역할을 수행. + * 최종적으로 각 하위 요소별 스타일이 CSSProperties 객체로 매핑된 객체를 반환. + * 이때 하위 요소별 스타일객체는 평탄화 되어있습니다. + */ +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); + }); + + return nodeStyleObj; +} From ab314d7261d0f2058c1e5316981b34c2718031d7 Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 1 Dec 2025 14:50:14 +0900 Subject: [PATCH 06/22] =?UTF-8?q?=E2=9C=A8=20=EB=85=B8=EB=93=9C=EB=A5=BC?= =?UTF-8?q?=20=EA=B0=90=EC=8B=B8=EB=8A=94=20wrapper=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 노드 렌더러가 렌더링한 노드 컴포넌트를 감싸서 유저가 자유롭게 위치 변경 가능하게 해준다. --- packages/ui/src/core/DraggableNodeWrapper.tsx | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 packages/ui/src/core/DraggableNodeWrapper.tsx diff --git a/packages/ui/src/core/DraggableNodeWrapper.tsx b/packages/ui/src/core/DraggableNodeWrapper.tsx new file mode 100644 index 0000000..9b149b7 --- /dev/null +++ b/packages/ui/src/core/DraggableNodeWrapper.tsx @@ -0,0 +1,51 @@ +import { useBuilderMode } from "context/builderMode"; +import { CSSProperties } from "react"; +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 { mode } = useBuilderMode(); + const wrapperStyle: CSSProperties = { + position: "absolute", // 핵심: 캔버스 내에서 자유 배치 + left: node["layout"].x, + top: node["layout"].y, + width: node["layout"].width, + height: node["layout"].height, + zIndex: node["layout"].zIndex, + + // 선택되었을 때 시각적 피드백 (테두리 등) + outline: mode === "editor" && isSelected ? "2px solid blue" : "none", + cursor: mode === "editor" ? "move" : "default", + }; + + return ( +
mode === "editor" && onDragStart(e, node.id)} + className="" //TODO - 필요시 클래스네임 추가(dnd상황에서 스타일 강조 등) + > + {/* 실제 컴포넌트(Hero 등)는 이 안에 렌더링됨 */} + {children} + + {/* 리사이즈 핸들 등은 에디터 모드에서만 오버레이로 표시 */} + {/* TODO-추후 호버시 리사이즈 핸들 렌더링 하도록 수정 필요! */} + {isSelected && mode === "editor" && ( + <> +
+ {/* TODO-기타 리사이즈 핸들들 렌더링은 이곳에서 ... */} + + )} +
+ ); +} From 85a6ac4fb97f29c55b3c04c916e23da3676b047e Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 1 Dec 2025 15:50:10 +0900 Subject: [PATCH 07/22] =?UTF-8?q?=E2=9C=A8=20=ED=9E=88=EC=96=B4=EB=A1=9C?= =?UTF-8?q?=20=EB=85=B8=EB=93=9C=20=EB=A0=8C=EB=8D=94=EB=9F=AC=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 전체 레이아웃 사이즈, 위치는 상위 wrapper컴포넌트에게 위임하고, 해당 노드 렌더러 컴포넌트는 컨텐츠 렌더링, 내부 요소 렌더링에 집중하도록 구현 --- packages/ui/src/components/Hero.tsx | 68 ++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/components/Hero.tsx b/packages/ui/src/components/Hero.tsx index 54b38df..89d9fee 100644 --- a/packages/ui/src/components/Hero.tsx +++ b/packages/ui/src/components/Hero.tsx @@ -1,7 +1,8 @@ -import applyStyles from "../utils/applyStyles"; import { NodeComponentProps } from "../types/component"; import { HeroNode } from "../types/nodes"; import { useBuilderMode } from "context/builderMode"; +import Image from "next/image"; +import processNodeStyles from "../utils/style"; export default function HeroComponent({ node, @@ -9,26 +10,63 @@ export default function HeroComponent({ style, }: NodeComponentProps) { const { mode } = useBuilderMode(); //현재 모드 확인 - const { heading, subHeading, button, backgroundImage } = props; - const inlineStyles = applyStyles(style); - const bgImageUrl = backgroundImage ? backgroundImage : null; + //이 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 ( -
-

{heading}

- {subHeading &&

{subHeading}

} - {button && ( - + //TODO - 노드들이 드래그 앤 드롭될때 위치가 자유롭게 변하게 할 수 있어야한다. +
+ {/* 배경 이미지 영역 */} + {image?.url && ( +
+ {image.alt +
+
)} -
+ + {/* --- 콘텐츠 영역 (z-index를 높여서 이미지 위에 표시) --- */} +
+

+ {heading} +

+ {subHeading &&

{subHeading}

} + {button && ( + + {button?.text} + + )} +
+
); } From 5100dd8978879731c5fc9ddc97a1b687bbd40c45 Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 1 Dec 2025 22:51:42 +0900 Subject: [PATCH 08/22] =?UTF-8?q?=EB=8B=A8=EC=88=9C=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/src/{renderer => core}/NodeRenderer.tsx | 0 packages/ui/src/utils/{style.ts => processNodeStyles.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename packages/ui/src/{renderer => core}/NodeRenderer.tsx (100%) rename packages/ui/src/utils/{style.ts => processNodeStyles.ts} (100%) diff --git a/packages/ui/src/renderer/NodeRenderer.tsx b/packages/ui/src/core/NodeRenderer.tsx similarity index 100% rename from packages/ui/src/renderer/NodeRenderer.tsx rename to packages/ui/src/core/NodeRenderer.tsx diff --git a/packages/ui/src/utils/style.ts b/packages/ui/src/utils/processNodeStyles.ts similarity index 100% rename from packages/ui/src/utils/style.ts rename to packages/ui/src/utils/processNodeStyles.ts From 6e21644f27eb919fc7c0c1c12f65ae713334e55e Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 1 Dec 2025 22:52:50 +0900 Subject: [PATCH 09/22] =?UTF-8?q?=F0=9F=93=9D=20=EB=8B=A8=EC=88=9C=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/src/types/styles.ts | 6 ++++-- packages/ui/src/utils/applyStyles.ts | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/types/styles.ts b/packages/ui/src/types/styles.ts index 8a2198c..f80cdc5 100644 --- a/packages/ui/src/types/styles.ts +++ b/packages/ui/src/types/styles.ts @@ -10,6 +10,7 @@ type ElementStyleKey = | "background" //배경 | "effects"; // boxShadow, opacity, borderRadius 등 export interface ElementStyle + //FIXME - root 스타일은 크기(dimensions),레이아웃(layout)을 제외한 나머지(배경, 테두리, 패딩)만 담당하도록 수정해야한다. extends Partial> { // 2. HTML 클래스 (Tailwind 유틸리티 등) -> 추후 바이브 코딩의 결과물을 받았을때 사용될 속성입니다. className?: string; @@ -20,11 +21,12 @@ export interface ElementStyle } export type NodeStyleKey = - | "root" //필수: 최상위 컨테이너 + | "root" //필수: 최상위 컨테이너 -> 항상 최상위 컨테이너 스타일은 root로 지정 | "button" // 옵션: 제목 | "heading" // 옵션: 부제목 | "subHeading" // 옵션: 버튼 - | "image"; // 옵션: 이미지 + | "image" // 옵션: 이미지 + | "text"; // 옵션: 텍스트 // 2. 컴포넌트 전체 스타일: 부위별(Key)로 ElementStyle을 가짐 diff --git a/packages/ui/src/utils/applyStyles.ts b/packages/ui/src/utils/applyStyles.ts index 71e2683..b4d9c0c 100644 --- a/packages/ui/src/utils/applyStyles.ts +++ b/packages/ui/src/utils/applyStyles.ts @@ -1,5 +1,5 @@ //참고 자료->https://www.notion.so/Object-assign-2b175c9287fa8077b766de146b87a7e2?source=copy_link -//추후 성능 개선 필요(메모이제이션) +//TODO-추후 성능 개선 필요(메모이제이션) import { CSSProperties } from "react"; import { ElementStyle } from "../types/styles"; @@ -19,6 +19,7 @@ export default function applyStyles( const combinedStyles = {}; for (const key in styleData) { //카테고리별로 중첩된 스타일 데이터를 평탄화 시킴. + //⭐️ className은 평탄화 작업에서 안전하게 제외합니다. if (key !== "className" && typeof styleData[key] === "object") { Object.assign(combinedStyles, styleData[key]); //스프레드 연산자 오버헤드 위험 From c2ada183dcf3da3a66a0ecea62c4a589114de26f Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 1 Dec 2025 22:53:36 +0900 Subject: [PATCH 10/22] =?UTF-8?q?=E2=9C=A8=20Text=20=EB=85=B8=EB=93=9C=20?= =?UTF-8?q?=EB=8B=B4=EB=8B=B9=20=EB=85=B8=EB=93=9C=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=9F=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/src/components/Text.tsx | 24 ++++++++++++++++++++++++ packages/ui/src/types/nodes.ts | 7 ++++++- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 packages/ui/src/components/Text.tsx diff --git a/packages/ui/src/components/Text.tsx b/packages/ui/src/components/Text.tsx new file mode 100644 index 0000000..ca949f1 --- /dev/null +++ b/packages/ui/src/components/Text.tsx @@ -0,0 +1,24 @@ +import { NodeComponentProps, TextNode } from "types"; +import processNodeStyles from "utils/processNodeStyles"; + +export default function TextComponent({ + node, + props, + style, +}: NodeComponentProps) { + const { text, level = "h2" } = props; + const Tag = level; + + const nodeStyleObj = processNodeStyles(style); + + return ( +
+ {text} + {/* TODO-만약 텍스트 컴포넌트가 사진같은 정적 파일도 렌더링 해야한다면? */} +
+ ); +} diff --git a/packages/ui/src/types/nodes.ts b/packages/ui/src/types/nodes.ts index 798abc8..a92bdd8 100644 --- a/packages/ui/src/types/nodes.ts +++ b/packages/ui/src/types/nodes.ts @@ -39,6 +39,10 @@ export interface HeadingNode extends BaseNode { type: "Heading"; props: componentProps.HeadingProps; } +export interface TextNode extends BaseNode { + type: "Text"; + props: componentProps.TextProps; +} export interface ButtonNode extends BaseNode { type: "Button"; @@ -57,4 +61,5 @@ export type WcxNode = | ImageNode | HeadingNode | ButtonNode - | ContainerNode; + | ContainerNode + | TextNode; From fc14d98e459206f533f21c97d113a514375887b3 Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 1 Dec 2025 22:55:31 +0900 Subject: [PATCH 11/22] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20text=20=EC=A0=84?= =?UTF-8?q?=EC=9A=A9=20props=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/src/types/componentProps.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/ui/src/types/componentProps.ts b/packages/ui/src/types/componentProps.ts index fbe212f..c99c434 100644 --- a/packages/ui/src/types/componentProps.ts +++ b/packages/ui/src/types/componentProps.ts @@ -36,6 +36,11 @@ export interface ButtonProps { target?: "_blank" | "_self"; } +export interface TextProps { + text: string; + level: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; +} + // 5. Container 컴포넌트 Props (보통 비어있거나 ID 정도만 가짐) export interface ContainerProps { id?: string; From 9ae00ed5644d95751ad98bdf276964ed17b36673 Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 1 Dec 2025 22:58:43 +0900 Subject: [PATCH 12/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EB=85=B8=EB=93=9C?= =?UTF-8?q?=20=EB=A0=8C=EB=8D=94=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit className을 style객체에서 직접 참조하도록 수정 -> 인라인 스타일로 변경할때 className을 제외하고 추출하기때문에 className은 style객체에서 직접 참조해야한다. style추출할때 processNodeStyles함수 사용하도록 수정완료 --- packages/ui/src/components/Heading.tsx | 13 +++++++---- packages/ui/src/components/Hero.tsx | 31 ++++++++++++++++++++------ packages/ui/src/components/Image.tsx | 9 ++++---- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/components/Heading.tsx b/packages/ui/src/components/Heading.tsx index b77c1e3..37099d3 100644 --- a/packages/ui/src/components/Heading.tsx +++ b/packages/ui/src/components/Heading.tsx @@ -1,6 +1,6 @@ +import processNodeStyles from "utils/processNodeStyles"; import { NodeComponentProps } from "../types"; import { HeadingNode } from "../types/nodes"; -import applyStyles from "../utils/applyStyles"; export default function HeadingComponent({ node, @@ -9,12 +9,17 @@ export default function HeadingComponent({ children, }: NodeComponentProps) { const { text, level = "h2" } = props; - const inlineStyle = applyStyles(style); + const nodeStyleObj = processNodeStyles(style); + const Tag = level; return ( -
- {text} +
+ {text} {/* 컨테이너일 경우 이곳에 children이 렌더링 되야 한다. */} {children}
diff --git a/packages/ui/src/components/Hero.tsx b/packages/ui/src/components/Hero.tsx index 89d9fee..e5a81ef 100644 --- a/packages/ui/src/components/Hero.tsx +++ b/packages/ui/src/components/Hero.tsx @@ -1,8 +1,7 @@ -import { NodeComponentProps } from "../types/component"; -import { HeroNode } from "../types/nodes"; import { useBuilderMode } from "context/builderMode"; import Image from "next/image"; -import processNodeStyles from "../utils/style"; +import processNodeStyles from "../utils/processNodeStyles"; +import { HeroNode, NodeComponentProps } from "types"; export default function HeroComponent({ node, @@ -29,12 +28,20 @@ export default function HeroComponent({ //TODO - 노드들이 드래그 앤 드롭될때 위치가 자유롭게 변하게 할 수 있어야한다.
{/* 배경 이미지 영역 */} {image?.url && ( -
+

{heading}

- {subHeading &&

{subHeading}

} + + {subHeading && ( +

+ {subHeading} +

+ )} {button && ( diff --git a/packages/ui/src/components/Image.tsx b/packages/ui/src/components/Image.tsx index f9b7a18..47fa24c 100644 --- a/packages/ui/src/components/Image.tsx +++ b/packages/ui/src/components/Image.tsx @@ -1,7 +1,7 @@ import Image from "next/image"; -import applyStyles from "../utils/applyStyles"; import { NodeComponentProps } from "../types/component"; import { ImageNode } from "../types/nodes"; +import processNodeStyles from "utils/processNodeStyles"; export default function ImageComponent({ node, @@ -9,14 +9,15 @@ export default function ImageComponent({ style, }: NodeComponentProps) { const { src, alt = "사용자의 이미지", caption } = props; - const inlineStyles = applyStyles(style); + const nodeStyleObj = processNodeStyles(style); return (
- {alt} + {alt} {caption &&
{caption}
}
); From f35e054a7213c0e95c37f6e3bd5a96209cb5d8cf Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 3 Dec 2025 22:12:30 +0900 Subject: [PATCH 13/22] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20Action=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 버튼 노드가 할 수 있는 일들을 정의한 타입입니다. --- packages/ui/src/types/props.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 packages/ui/src/types/props.ts diff --git a/packages/ui/src/types/props.ts b/packages/ui/src/types/props.ts new file mode 100644 index 0000000..0497338 --- /dev/null +++ b/packages/ui/src/types/props.ts @@ -0,0 +1,23 @@ +export type ActionType = + | "link" // 5. 링크 이동 + | "scroll" // 3. 스크롤 이동 + | "alert" // 2. 알림창 + | "modal" // 4. 모달 열기 + | "mutation"; // 1. 데이터 변경 (핵심!) + +export interface NodeAction { + type: ActionType; + + // 공통 페이로드 (링크 URL, 알림 메시지, 타겟 ID 등) + payload?: string; + + // mutation용 설정 + targetId?: string; // 변경할 노드의 ID (ex: "text-node-123") + mutationType?: "increment" | "setText" | "toggle"; // 구체적인 변경 방식 + value?: any; // 변경할 값 +} + +export interface ButtonProps { + text: string; + action?: NodeAction; +} From 26fafe749afe79e95768c188f975674a7497517f Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 3 Dec 2025 22:24:04 +0900 Subject: [PATCH 14/22] =?UTF-8?q?=E2=9C=A8=20=EB=85=B8=EB=93=9C=20?= =?UTF-8?q?=EB=A0=8C=EB=8D=94=EB=9F=AC=EB=93=A4=EC=9D=B4=20=EB=A7=8C?= =?UTF-8?q?=EB=93=A0=20=EB=85=B8=EB=93=9C=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EB=93=A4=EC=9D=B4=20=EB=8F=99=EC=A0=81=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=8F=99=EC=9E=91=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=8F=84=EC=99=80=EC=A3=BC=EB=8A=94=20?= =?UTF-8?q?=EC=BB=A8=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 특정 행동을 트리거하는 버튼 노드가 타겟 노드에게 동적으로 변하도록 도와주는 중개소 역할인 context를 구현했습니다. 직접적으로 getElementById를 통해 변화시키면 리액트의 라이프 사이클을 무시하므로 context를 사용했습니다. --- packages/ui/src/context/runtimeContext.tsx | 49 ++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 packages/ui/src/context/runtimeContext.tsx diff --git a/packages/ui/src/context/runtimeContext.tsx b/packages/ui/src/context/runtimeContext.tsx new file mode 100644 index 0000000..b8d4246 --- /dev/null +++ b/packages/ui/src/context/runtimeContext.tsx @@ -0,0 +1,49 @@ +"use client"; + +/* +노드 렌더러들이 만든 노드 컴포넌트가 정적이지만, 이를 동적으로 변경해주기위해 필요한 장치입니다. +버튼 컴포넌트를 설계하다 필요를 느껴 해당 context를 고안했습니다. +버튼을 클릭하면 동적으로 웹사이트가 변경되야하면 어떡하지? 에서 해당 컨텍스트에 타겟 id와 행동을 전달해 타겟 노드에 동작을 전달하기 위한 중간 장치입니다. +*/ + +import { createContext, ReactNode, useContext, useState } from "react"; + +// 런타임에 변경된 노드들의 상태를 저장하는 객체 +// 예: { "node-123": { text: "11" }, "modal-456": { isOpen: true } } +type RuntimeState = Record; + +interface RuntimeContextType { + state: RuntimeState; + updateNodeState: (id: string, newState: any) => void; +} + +const RuntimeContext = createContext(null); + +//TODO-빠른 구현을 위해 Context를 사용했지만 하나의 상태가 바뀌더라도 해당 context를 구독하는 다른 노드들도 리렌더링이 발생하는 위험이 존재합니다. 꼭 추후에 상태 관리 방식을 리팩토링 해야합니다. +export function RuntimeProvider({ children }: { children: ReactNode }) { + const [state, setState] = useState({}); + + function updateNodeState(id: string, partial: any) { + setState((prev) => ({ + ...prev, + [id]: { ...prev[id], ...partial }, + })); + } + + const contextValue = { + state, + updateNodeState, + }; + + return ( + + {children} + + ); +} + +export function useRuntimeState() { + const context = useContext(RuntimeContext); + if (!context) throw new Error("RuntimeProvider is not Here"); + return context; +} From 2f234fdbe035cebafaeb6d6450f2e9b704ed7ddc Mon Sep 17 00:00:00 2001 From: y-minion Date: Thu, 4 Dec 2025 13:51:05 +0900 Subject: [PATCH 15/22] =?UTF-8?q?=E2=9C=A8=20=EB=B2=84=ED=8A=BC=EC=9D=98?= =?UTF-8?q?=20=EC=95=A1=EC=85=98=EB=93=A4=EC=9D=84=20js=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=ED=99=98=ED=95=B4=EC=A3=BC=EB=8A=94=20?= =?UTF-8?q?=ED=9B=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DB에는 js코드를 그대로 저장할 수 없기에, 객체로 된 행동 요구사항을 js코드로 전환해주는 역할을 합니다. --- packages/ui/src/hooks/useActionHandler.tsx | 46 ++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 packages/ui/src/hooks/useActionHandler.tsx diff --git a/packages/ui/src/hooks/useActionHandler.tsx b/packages/ui/src/hooks/useActionHandler.tsx new file mode 100644 index 0000000..d60b2cf --- /dev/null +++ b/packages/ui/src/hooks/useActionHandler.tsx @@ -0,0 +1,46 @@ +"use Rounter"; + +import { useRuntimeState } from "context/runtimeContext"; +import { useRouter } from "next/router"; +import { NodeAction } from "types/props"; + +export function useActionHandler(action?: NodeAction) { + const router = useRouter(); + const { state, updateNodeState } = useRuntimeState(); + + function excute() { + if (!action) return; + + switch (action.type) { + // Alert 띄우기 + case "alert": + alert(action.payload || "알림"); + break; + + //TODO-스크롤할때의 액션 옵션도 추후에 입력에 따라서 사용자가 커스텀 할 수 있도록 개션해야합니다. + case "scroll": + const element = document.querySelector( + `[data-component-id=${action.payload}]`, + ); + element?.scrollIntoView({ + block: "center", + behavior: "smooth", + inline: "nearest", + }); + + break; + + //모달 띄우기 (모달 노드의 isOpen 상태를 변경) + case "modal": + if (action.targetId) { + updateNodeState(action.targetId, { isOpen: true }); + } + break; + + //링크이동 + case "link": + if (action.payload) router.push(action.payload); + break; + } + } +} From 46fa5a82cd664a32dfab2c1248d0616b85026b7c Mon Sep 17 00:00:00 2001 From: y-minion Date: Thu, 4 Dec 2025 17:45:41 +0900 Subject: [PATCH 16/22] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=EB=85=B8=EB=93=9C=20=EC=A0=84=EC=9A=A9=20props,style?= =?UTF-8?q?=20=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80=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/types/nodes.ts | 8 +++++++- packages/ui/src/types/styles.ts | 6 +++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/types/nodes.ts b/packages/ui/src/types/nodes.ts index a92bdd8..88d8e6b 100644 --- a/packages/ui/src/types/nodes.ts +++ b/packages/ui/src/types/nodes.ts @@ -54,6 +54,11 @@ export interface ContainerNode extends BaseNode { props: componentProps.ContainerProps; } +export interface ModalNode extends BaseNode { + type: "Modal"; + props: componentProps.ModalProps; +} + // 3. 통합 노드 타입 // 이제 WcxNode 타입을 쓰면 type 체크 시 props가 자동 추론. export type WcxNode = @@ -62,4 +67,5 @@ export type WcxNode = | HeadingNode | ButtonNode | ContainerNode - | TextNode; + | TextNode + | ModalNode; diff --git a/packages/ui/src/types/styles.ts b/packages/ui/src/types/styles.ts index f80cdc5..173d883 100644 --- a/packages/ui/src/types/styles.ts +++ b/packages/ui/src/types/styles.ts @@ -22,9 +22,9 @@ export interface ElementStyle export type NodeStyleKey = | "root" //필수: 최상위 컨테이너 -> 항상 최상위 컨테이너 스타일은 root로 지정 - | "button" // 옵션: 제목 - | "heading" // 옵션: 부제목 - | "subHeading" // 옵션: 버튼 + | "button" // 옵션: 버튼 + | "heading" // 옵션: 제목 + | "subHeading" // 옵션: 부제목 | "image" // 옵션: 이미지 | "text"; // 옵션: 텍스트 From 76b7dbbfdd1436fa028aa85f52ac56d4b6f9a0a7 Mon Sep 17 00:00:00 2001 From: y-minion Date: Thu, 4 Dec 2025 17:46:42 +0900 Subject: [PATCH 17/22] =?UTF-8?q?=E2=9C=A8=20=EB=AA=A8=EB=8B=AC=20?= =?UTF-8?q?=EB=85=B8=EB=93=9C=20=EB=A0=8C=EB=8D=94=EB=9F=AC=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 모달을 렌더링 하는 렌더러 컴포넌트 구현 완료. 에디터 화면에서 추가로 DND를 통해 하위 노드를 받을 수 있도록 children props를 받도록 설정했습니다. --- packages/ui/src/components/Modal.tsx | 89 ++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 packages/ui/src/components/Modal.tsx diff --git a/packages/ui/src/components/Modal.tsx b/packages/ui/src/components/Modal.tsx new file mode 100644 index 0000000..0c87419 --- /dev/null +++ b/packages/ui/src/components/Modal.tsx @@ -0,0 +1,89 @@ +import { useBuilderMode } from "context/builderMode"; +import { useRuntimeState } from "context/runtimeContext"; +import { useRef } from "react"; +import { ModalNode, NodeComponentProps } from "types"; +import processNodeStyles from "utils/processNodeStyles"; + +/** + * + * @param param0 + * @returns + * + * 해당 모달 렌더러를 에디터에서 보여주고 싶다면 기본 상태는 isOpen:flase 이므로 에디터에 한해서만 강제로 updateNodeState(id, { isOpen: true })를 호출해서 보여줘야합니다. + */ +export default function Modal({ + node, + props, + style, + children, //모달안에 들어갈 버튼, 텍스트 등이 children으로 올 수 있습니다. +}: NodeComponentProps) { + const dialogRef = useRef(null); + + const curNodeId = node.id; + + const { mode } = useBuilderMode(); + + //Context를 통한 상태 구독 + //TODO-만약 context의 형태가 context API에서 zustand로 바뀐다면 해당 로직도 수정되야 합니다. + const { state, updateNodeState } = useRuntimeState(); + + const isOpen = state[curNodeId]?.isOpen ?? false; + + const { title, showCloseButton, closeOnOverlayClick } = props; + + const nodeStyleObj = processNodeStyles(style); + + //닫기 핸들러 + function closeHandler(e?: React.MouseEvent) { + if (mode === "editor") return; + + e?.stopPropagation(); + + updateNodeState(curNodeId, { isOpen: false }); + } + + function overlayClickHandler(e?: React.MouseEvent) { + if (props.closeOnOverlayClick === false) return; + if (e?.target !== dialogRef.current) return; + //오직 ::backdrop에 이벤트가 발생했을 경우에만 발생합니다. + closeHandler(e); + } + + if (!isOpen) return null; + + //굳이 div로 오버레이가 필요 없을듯..? ::backdrop과 dialog에서 제공하는 .showModal() 사용해보기 + // (closeOnOverlayClick 존재 유무에 따라 div태그의 핸들러 동작을 결정하자.) + + //기본적인 모달 태그는 dialog로 결정 + //이때 배경 클릭시 + return ( + + {/* 실제 모달 컨테이너 */} +
e.stopPropagation()} + className={`${style.root?.className || ""} relative min-h-[200px] min-w-[300px] rounded-lg bg-white shadow-xl`} + style={nodeStyleObj.root} + > + {/* 모달 헤더 */} + {(props.title || props.showCloseButton) && ( +
+ {props.title &&

{props.title}

} + + {/* TODO-svg 공통 모듈 작업 필요 */} + {props.showCloseButton && } +
+ )} + + {/* [Body] 자식 노드들(버튼, 이미지, 텍스트)이 여기에 렌더링 됨 */} +
{children}
+
+
+ ); +} From e4762e2c961a3e16ddb433cbdeb8ee483479fc21 Mon Sep 17 00:00:00 2001 From: y-minion Date: Thu, 4 Dec 2025 17:48:04 +0900 Subject: [PATCH 18/22] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EB=85=B8=EB=93=9C=20=EB=A0=8C=EB=8D=94=EB=9F=AC=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20props=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/src/types/componentProps.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/ui/src/types/componentProps.ts b/packages/ui/src/types/componentProps.ts index c99c434..2abb287 100644 --- a/packages/ui/src/types/componentProps.ts +++ b/packages/ui/src/types/componentProps.ts @@ -46,3 +46,14 @@ export interface ContainerProps { id?: string; tagName?: "div" | "section" | "article"; } + +export interface ModalProps { + //모달의 제목 + title?: string; + + //닫기 버튼 렌더링 여부 + showCloseButton?: boolean; + + //백그라운드 클릭시 닫기 여부 + closeOnOverlayClick?: boolean; +} From 28a027bb20128a0bd6d57c3d64cf63562abb1b85 Mon Sep 17 00:00:00 2001 From: y-minion Date: Thu, 4 Dec 2025 21:20:28 +0900 Subject: [PATCH 19/22] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20=EC=95=A1?= =?UTF-8?q?=EC=85=98=20=EA=B4=80=EB=A0=A8=20=ED=83=80=EC=9E=85=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/src/types/componentProps.ts | 5 +++-- packages/ui/src/types/{props.ts => nodeAction.ts} | 5 ----- 2 files changed, 3 insertions(+), 7 deletions(-) rename packages/ui/src/types/{props.ts => nodeAction.ts} (88%) diff --git a/packages/ui/src/types/componentProps.ts b/packages/ui/src/types/componentProps.ts index 2abb287..2c37701 100644 --- a/packages/ui/src/types/componentProps.ts +++ b/packages/ui/src/types/componentProps.ts @@ -1,5 +1,7 @@ //노드의 타입별로 달라지는 props의 타입을 정의. +import { NodeAction } from "./nodeAction"; + // 1. Hero 컴포넌트 Props export interface HeroProps { heading: string; @@ -32,8 +34,7 @@ export interface HeadingProps { // 클릭 이벤트도 들어가야 하는거 아닌가? export interface ButtonProps { text: string; - link: string; - target?: "_blank" | "_self"; + action?: NodeAction; } export interface TextProps { diff --git a/packages/ui/src/types/props.ts b/packages/ui/src/types/nodeAction.ts similarity index 88% rename from packages/ui/src/types/props.ts rename to packages/ui/src/types/nodeAction.ts index 0497338..f8ed6e3 100644 --- a/packages/ui/src/types/props.ts +++ b/packages/ui/src/types/nodeAction.ts @@ -16,8 +16,3 @@ export interface NodeAction { mutationType?: "increment" | "setText" | "toggle"; // 구체적인 변경 방식 value?: any; // 변경할 값 } - -export interface ButtonProps { - text: string; - action?: NodeAction; -} From ebd26bf19c4a1a2913f92240dfee0f48fa63cc70 Mon Sep 17 00:00:00 2001 From: y-minion Date: Thu, 4 Dec 2025 21:20:46 +0900 Subject: [PATCH 20/22] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=B0=B8=EC=A1=B0=20=EA=B2=BD=EB=A1=9C=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/src/hooks/useActionHandler.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/hooks/useActionHandler.tsx b/packages/ui/src/hooks/useActionHandler.tsx index d60b2cf..0b97778 100644 --- a/packages/ui/src/hooks/useActionHandler.tsx +++ b/packages/ui/src/hooks/useActionHandler.tsx @@ -2,7 +2,7 @@ import { useRuntimeState } from "context/runtimeContext"; import { useRouter } from "next/router"; -import { NodeAction } from "types/props"; +import { NodeAction } from "types/nodeAction"; export function useActionHandler(action?: NodeAction) { const router = useRouter(); @@ -41,6 +41,10 @@ export function useActionHandler(action?: NodeAction) { case "link": if (action.payload) router.push(action.payload); break; + + //TODO-추후 필요한 기능 추가 예정(API 호출 등등...) } } + + return excute; } From ae70f0b443952bca48cd589e5887982300977f1f Mon Sep 17 00:00:00 2001 From: y-minion Date: Thu, 4 Dec 2025 21:21:41 +0900 Subject: [PATCH 21/22] =?UTF-8?q?=E2=9C=A8=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EB=85=B8=EB=93=9C=20=EB=A0=8C=EB=8D=94=EB=9F=AC=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 액션 객체를 알맞은 액션 핸들러 로직으로 변경한뒤 동작하도록 구현 완료 --- packages/ui/src/components/Button.tsx | 43 +++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 packages/ui/src/components/Button.tsx diff --git a/packages/ui/src/components/Button.tsx b/packages/ui/src/components/Button.tsx new file mode 100644 index 0000000..e87446e --- /dev/null +++ b/packages/ui/src/components/Button.tsx @@ -0,0 +1,43 @@ +import { useBuilderMode } from "context/builderMode"; +import { useActionHandler } from "hooks/useActionHandler"; +import { ButtonNode, NodeComponentProps } from "types"; +import processNodeStyles from "utils/processNodeStyles"; + +export default function Button({ + node, + props, + style, +}: NodeComponentProps) { + const { mode } = useBuilderMode(); + const { text, action } = props; + + //스타일 변환(className은 제외, 오직 CSS속성만) + const nodeStyleObj = processNodeStyles(style); + + //액션 함수 생성 + const excuteAction = useActionHandler(action); + + //클릭 핸들러 + function clickHandler(e: React.MouseEvent) { + //에디터 모드에서는 액션 실행 x + if (mode === "editor") { + e.preventDefault(); + return; + } + + //라이브 모드: 실제 액션 실행 + excuteAction(); + } + + return ( + + ); +} From 7293c809a14ab8ad09325bf68c9879cbdf25b1c9 Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 17 Dec 2025 19:31:45 +0900 Subject: [PATCH 22/22] =?UTF-8?q?Nago=20=EC=9C=84=ED=95=9C=20=ED=98=91?= =?UTF-8?q?=EC=97=85=20=EA=B0=80=EC=9D=B4=EB=93=9C=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/editor/WORK_DIVISION.md | 119 +++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 apps/editor/WORK_DIVISION.md diff --git a/apps/editor/WORK_DIVISION.md b/apps/editor/WORK_DIVISION.md new file mode 100644 index 0000000..f0662b1 --- /dev/null +++ b/apps/editor/WORK_DIVISION.md @@ -0,0 +1,119 @@ +# WebCreatorX - 팀 업무 분담 가이드 + +이 문서는 프로젝트의 효율적인 개발을 위해 팀원 간의 업무 영역과 협업 포인트를 정의합니다. +서로의 작업 영역을 침범하지 않으면서, 공통 상태(Store)와 데이터 구조를 기반으로 유기적으로 연결되도록 설계되었습니다. + +--- + +## 0. 들어가기에 앞서 +- 프로젝트 구조가 모노레포로 변경되었습니다! monorepoGuide.md 를 꼭 먼저 읽어주세요! + +## 1. 프로젝트 개요 및 아키텍처 +**WebCreatorX**는 노드 기반의 웹사이트 빌더입니다. 사용자는 좌측 사이드바에서 컴포넌트를 추가하고, 중앙 캔버스에서 확인하며, 우측 사이드바에서 속성을 편집합니다. + +### 핵심 데이터 구조 (`WcxNode`) +모든 컴포넌트(Hero, Image, Text 등)는 `WcxNode`라는 공통 타입을 가집니다. +- **정의 위치**: `packages/ui/src/types/nodes.ts` +- **핵심 필드**: + - `id`: 고유 식별자 (UUID) + - `type`: 컴포넌트 종류 (Hero, Image, Button 등) + - `props`: 해당 컴포넌트의 데이터 (텍스트 내용, 이미지 URL 등) + - `style`: 스타일 정보 + +--- + +## 2. 업무 분담 (R&R) + +### 🧑‍💻 Milo (Core & Renderer) +- **책임 영역**: 에디터 중앙 캔버스, 노드 렌더링, 전역 상태(Store) 설계, 서버 통신 +- **주요 작업**: + 1. **Node Renderer**: 서버에서 받아온 `nodes` 데이터를 순회하며 실제 화면에 그립니다. + 2. **Interaction**: 캔버스 상의 요소를 클릭했을 때, 해당 노드의 `id`를 `selectedId` 상태로 업데이트합니다. + 3. **Drag & Drop (추후)**: 캔버스 내에서의 위치 이동을 담당합니다. (DND 라이브러리 사용 예정) + +### 🧑‍💻 Nago (Sidebars & Controls) +- **책임 영역**: 좌측 사이드바(생성), 우측 사이드바(편집), UI 컨트롤 +- **주요 작업**: + 1. **좌측 사이드바 (Node Creator)**: 사용 가능한 컴포넌트 목록을 보여주고, 클릭 시 새로운 노드를 생성하여 캔버스에 추가합니다. + 2. **우측 사이드바 (Property Editor)**: 현재 선택된(`selectedId`) 노드의 정보를 보여주고, 사용자가 값을 수정하면 이를 실시간으로 반영합니다. + +--- + +## 3. 협업 포인트: 전역 상태 (Zustand Store) + +두 팀원의 작업은 **Zustand Store**를 통해 만납니다. 팀원은 아래 Store Hook이 존재한다고 가정하고 개발을 진행하면 됩니다. + +```typescript +// apps/editor/src/store/useEditorStore.ts (예시 구조) + +interface EditorState { + nodes: Record; // 전체 노드 데이터 (ID를 키로 사용) + selectedId: string | null; // 현재 선택된 노드 ID + + // Actions + addNode: (node: WcxNode) => void; + updateNode: (id: string, partialNode: Partial) => void; + selectNode: (id: string | null) => void; +} +``` + +--- + +## 4. Nago 합류 상세 가이드 + +### ✅ Task 1: 좌측 사이드바 - 노드 생성 +**목표**: 사용자가 새로운 컴포넌트를 추가할 수 있는 패널을 만듭니다. + +**구현 내용**: +1. `packages/ui/src/types/nodes.ts`에 정의된 모든 노드 타입(Hero, Text, Image 등)을 버튼 형태로 나열합니다.(완자가 만든 피그마 기획서 참고) +2. 버튼 클릭 시, 해당 타입에 맞는 **기본 노드 객체**를 생성해야 합니다. + - 예: Text 노드 생성 시 `id: uuid()`, `type: 'Text'`, `props: { text: 'New Text', level: 'h1' }` 등의 초기값을 가진 객체 생성. +3. 생성된 객체를 `useEditorStore`의 `addNode` 함수를 통해 상태에 추가합니다. + +**참고**: UI 디자인은 완자가 만든 피그마 기획서를 참고하여 구현해주세요. + +### ✅ Task 2: 우측 사이드바 - 속성 편집 (Manifest) +**목표**: 선택된 노드의 세부 내용을 수정하는 패널을 만듭니다. + +**구현 내용**: +1. `useEditorStore`에서 `selectedId`와 `nodes`를 구독합니다. +2. **조건부 렌더링**: + - `selectedId`가 `null`이면: "요소를 선택해주세요" 같은 안내 메시지 표시. + - `selectedId`가 있으면: `nodes[selectedId]`를 가져와서 편집 UI 표시. +3. **Switch Case 분기**: + - 선택된 노드의 `type`에 따라 다른 컴포넌트를 렌더링해야 합니다. + - `Hero` 타입 -> `HeroControl` 컴포넌트 (Heading 입력, 이미지 업로드, 버튼 텍스트 입력창 등) + - `Image` 타입 -> `ImageControl` 컴포넌트 (URL 입력, Alt 입력창 등) +4. **데이터 업데이트**: + - 입력창의 값이 변할 때마다 `updateNode(selectedId, { props: { ...newProps } })`를 호출하여 실시간으로 반영합니다. + +### 💡 팁 +- **컴포넌트 재사용**: 각 속성 편집기(Input, Select, Toggle 등)는 `RightSidebar` 내부에서만 쓰이지 않고 나중에도 쓰일 수 있으니, 작고 재사용 가능한 'Control Component'로 쪼개서 개발하는 것이 좋습니다. +- **타입 안전성**: `packages/ui`에 있는 `WcxNode`, `HeroNode` 등의 타입을 적극 활용하여, 잘못된 속성을 수정하는 일을 방지하세요. + +### 중요 사항! +- 위에서 언급한 기획들은 대략적으로 서술한 것입니다. 실제로 구현에 필요한 자세한 정보들은 피그마의 기획서를 절대적으로 따라야합니다. +- 추가로 궁금한 점이 있으면 디스코드로 언제든지 질문해주세요! +--- + +## 5. 폴더 구조 제안 (apps/editor/src) +``` +src/ +├── components/ +│ ├── editor/ # (Milo) 캔버스, 렌더러 +│ │ ├── Canvas.tsx +│ │ └── NodeRenderer.tsx +│ │ +│ ├── sidebar/ # (Nago) 사이드바 +│ │ ├── LeftSidebar.tsx +│ │ ├── RightSidebar.tsx +│ │ └── controls/ # (Shared) 속성 편집용 작은 컴포넌트들 +│ │ ├── TextInput.tsx +│ │ └── ColorPicker.tsx +│ │ +│ └── layout/ # 전체 레이아웃 (Header, Main, Sidebars 배치) +│ └── EditorLayout.tsx +│ +└── store/ + └── useEditorStore.ts # 전역 상태 +```