From 7342e80ccec5ad0cc34f52258a25b2cfe1cb2bc9 Mon Sep 17 00:00:00 2001 From: y-minion Date: Thu, 18 Dec 2025 21:10:54 +0900 Subject: [PATCH 01/14] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EA=B8=B0=EB=8A=A5=20=EB=B3=80=EA=B2=BD=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B8=ED=95=9C=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 | 16 +++++++++------- packages/ui/src/types/project.ts | 6 ++++++ 2 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 packages/ui/src/types/project.ts diff --git a/packages/ui/src/types/componentProps.ts b/packages/ui/src/types/componentProps.ts index 2c37701..fd30e87 100644 --- a/packages/ui/src/types/componentProps.ts +++ b/packages/ui/src/types/componentProps.ts @@ -1,6 +1,7 @@ //노드의 타입별로 달라지는 props의 타입을 정의. import { NodeAction } from "./nodeAction"; +import { WcxNode } from "./nodes"; // 1. Hero 컴포넌트 Props export interface HeroProps { @@ -49,12 +50,13 @@ export interface ContainerProps { } export interface ModalProps { - //모달의 제목 - title?: string; - - //닫기 버튼 렌더링 여부 - showCloseButton?: boolean; - - //백그라운드 클릭시 닫기 여부 + // 위치 프리셋 (핵심) + alignment: "center" | "top" | "bottom" | "left" | "right"; + // 오버레이 설정 + overlayColor?: string; // "bg-black/50" closeOnOverlayClick?: boolean; + // 애니메이션 프리셋 + animation?: "fade" | "slide-up" | "slide-left"; + + children: WcxNode[]; //모달 내부의 컨텐츠들 } diff --git a/packages/ui/src/types/project.ts b/packages/ui/src/types/project.ts new file mode 100644 index 0000000..14e88c2 --- /dev/null +++ b/packages/ui/src/types/project.ts @@ -0,0 +1,6 @@ +import { ModalNode } from "./nodes"; + +export interface ProjectData { + //pages: PageNode[]; //TODO-PageNode타입 구현 예정 + modal: ModalNode[]; // 모달은 별도 리스트로 관리 (성능 및 재사용성) +} From 59a5087a646ea18ff93d45c20036a16a350b03e2 Mon Sep 17 00:00:00 2001 From: y-minion Date: Thu, 18 Dec 2025 21:17:37 +0900 Subject: [PATCH 02/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=B4=20context=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 활성화된 id를 상태로 갖고 provider로 활성화된 모달의 id를 관리할 수 있도록 로직 수정 --- packages/ui/src/context/runtimeContext.tsx | 28 ++++++++++------------ 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/packages/ui/src/context/runtimeContext.tsx b/packages/ui/src/context/runtimeContext.tsx index b8d4246..58a462e 100644 --- a/packages/ui/src/context/runtimeContext.tsx +++ b/packages/ui/src/context/runtimeContext.tsx @@ -8,31 +8,27 @@ 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; + activeModalId: string | null; + openModal: (id: string) => void; + closeModal: () => void; } -const RuntimeContext = createContext(null); +const RuntimeContext = createContext(null); //TODO-빠른 구현을 위해 Context를 사용했지만 하나의 상태가 바뀌더라도 해당 context를 구독하는 다른 노드들도 리렌더링이 발생하는 위험이 존재합니다. 꼭 추후에 상태 관리 방식을 리팩토링 해야합니다. +// -> ModalHost를 도입해서 괜찮을듯? export function RuntimeProvider({ children }: { children: ReactNode }) { - const [state, setState] = useState({}); + const [activeModalId, setActiveModalId] = useState(null); + + const openModal = (id: string) => setActiveModalId(id); - function updateNodeState(id: string, partial: any) { - setState((prev) => ({ - ...prev, - [id]: { ...prev[id], ...partial }, - })); - } + const closeModal = () => setActiveModalId(null); const contextValue = { - state, - updateNodeState, + activeModalId, + openModal, + closeModal, }; return ( From dca8850cc5a7ccde60c6320c0dbace9faad7f689 Mon Sep 17 00:00:00 2001 From: y-minion Date: Thu, 18 Dec 2025 22:41:38 +0900 Subject: [PATCH 03/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=20=EB=A0=8C=EB=8D=94=EB=9F=AC=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 modalHost에 의해 사용되는 모달 렌더러를 고려해 전체적인 로직 수정 완료 --- packages/ui/src/components/Modal.tsx | 73 +++++++++++-------------- packages/ui/src/types/componentProps.ts | 2 - 2 files changed, 32 insertions(+), 43 deletions(-) diff --git a/packages/ui/src/components/Modal.tsx b/packages/ui/src/components/Modal.tsx index 0c87419..258a1d8 100644 --- a/packages/ui/src/components/Modal.tsx +++ b/packages/ui/src/components/Modal.tsx @@ -1,9 +1,10 @@ import { useBuilderMode } from "context/builderMode"; import { useRuntimeState } from "context/runtimeContext"; -import { useRef } from "react"; import { ModalNode, NodeComponentProps } from "types"; import processNodeStyles from "utils/processNodeStyles"; +//TODO-모달 IsOpen상태 관리를 어떻게 최적화 할 수 있을까? + /** * * @param param0 @@ -17,73 +18,63 @@ export default function Modal({ style, children, //모달안에 들어갈 버튼, 텍스트 등이 children으로 올 수 있습니다. }: NodeComponentProps) { - const dialogRef = useRef(null); - - const curNodeId = node.id; - + // Hooks & state const { mode } = useBuilderMode(); + const curNodeId = node.id; + const { + alignment, + overlayColor = "", + closeOnOverlayClick = "", + animation = "", + } = props; //Context를 통한 상태 구독 //TODO-만약 context의 형태가 context API에서 zustand로 바뀐다면 해당 로직도 수정되야 합니다. - const { state, updateNodeState } = useRuntimeState(); - const isOpen = state[curNodeId]?.isOpen ?? false; + + const { closeModal } = useRuntimeState(); + + // 위치 프리셋에 따른 CSS 클래스 매핑 + const alignmentStyles = { + center: "items-center justify-center", + top: "items-start justify-center pt-10", + bottom: "items-end justify-center pb-0", // 바텀 시트 스타일 + left: "items-center justify-start h-full", // 사이드바 + right: "items-center justify-end h-full", // 사이드바 + }; - const { title, showCloseButton, closeOnOverlayClick } = props; + const positionClass = alignmentStyles[alignment] || alignmentStyles.center; + //스타일 변환 const nodeStyleObj = processNodeStyles(style); //닫기 핸들러 function closeHandler(e?: React.MouseEvent) { if (mode === "editor") return; - e?.stopPropagation(); - - updateNodeState(curNodeId, { isOpen: false }); + closeModal(); } - function overlayClickHandler(e?: React.MouseEvent) { - if (props.closeOnOverlayClick === false) return; - if (e?.target !== dialogRef.current) return; - //오직 ::backdrop에 이벤트가 발생했을 경우에만 발생합니다. + function overlayClickHandler(e?: React.MouseEvent) { + if (closeOnOverlayClick === false) return; closeHandler(e); } - if (!isOpen) return null; - - //굳이 div로 오버레이가 필요 없을듯..? ::backdrop과 dialog에서 제공하는 .showModal() 사용해보기 - // (closeOnOverlayClick 존재 유무에 따라 div태그의 핸들러 동작을 결정하자.) - - //기본적인 모달 태그는 dialog로 결정 - //이때 배경 클릭시 return ( - overlayClickHandler(e)} > {/* 실제 모달 컨테이너 */}
e.stopPropagation()} - className={`${style.root?.className || ""} relative min-h-[200px] min-w-[300px] rounded-lg bg-white shadow-xl`} + className={`${style.root?.className || ""} pointer-events-auto bg-white shadow-2xl`} style={nodeStyleObj.root} > - {/* 모달 헤더 */} - {(props.title || props.showCloseButton) && ( -
- {props.title &&

{props.title}

} - - {/* TODO-svg 공통 모듈 작업 필요 */} - {props.showCloseButton && } -
- )} - {/* [Body] 자식 노드들(버튼, 이미지, 텍스트)이 여기에 렌더링 됨 */}
{children}
-
+ ); } diff --git a/packages/ui/src/types/componentProps.ts b/packages/ui/src/types/componentProps.ts index fd30e87..2bdf941 100644 --- a/packages/ui/src/types/componentProps.ts +++ b/packages/ui/src/types/componentProps.ts @@ -57,6 +57,4 @@ export interface ModalProps { closeOnOverlayClick?: boolean; // 애니메이션 프리셋 animation?: "fade" | "slide-up" | "slide-left"; - - children: WcxNode[]; //모달 내부의 컨텐츠들 } From 9ffa3bbd64a50c558194e93575ca07cfd074d8f7 Mon Sep 17 00:00:00 2001 From: y-minion Date: Fri, 19 Dec 2025 15:09:44 +0900 Subject: [PATCH 04/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20context=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=EA=B0=92=20=ED=83=80=EC=9E=85=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/context/runtimeContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/context/runtimeContext.tsx b/packages/ui/src/context/runtimeContext.tsx index 58a462e..7b6934f 100644 --- a/packages/ui/src/context/runtimeContext.tsx +++ b/packages/ui/src/context/runtimeContext.tsx @@ -14,7 +14,7 @@ interface RuntimeContextType { closeModal: () => void; } -const RuntimeContext = createContext(null); +const RuntimeContext = createContext(null); //TODO-빠른 구현을 위해 Context를 사용했지만 하나의 상태가 바뀌더라도 해당 context를 구독하는 다른 노드들도 리렌더링이 발생하는 위험이 존재합니다. 꼭 추후에 상태 관리 방식을 리팩토링 해야합니다. // -> ModalHost를 도입해서 괜찮을듯? From 55ce8edc1313c33f32fe8de6e8e416d6469fd519 Mon Sep 17 00:00:00 2001 From: y-minion Date: Fri, 19 Dec 2025 16:17:59 +0900 Subject: [PATCH 05/14] =?UTF-8?q?=E2=9C=A8=20=EB=AA=A8=EB=8B=AC=20?= =?UTF-8?q?=ED=98=B8=EC=8A=A4=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 상위에서 모달 렌더링을 총괄하는 매니저 컴포넌트(모달호스트) 구현 완료. 버튼 클릭시 모달이 렌더링되는 로직이 연결되어 있으면 modalHost가 DB에서 모달 데이터를 받아서 모달 렌더러에게 전달합니다. --- packages/ui/src/components/ModalHost.tsx | 22 ++++++++++++++++++++++ packages/ui/src/hooks/useProjectData.tsx | 8 ++++++++ 2 files changed, 30 insertions(+) create mode 100644 packages/ui/src/components/ModalHost.tsx create mode 100644 packages/ui/src/hooks/useProjectData.tsx diff --git a/packages/ui/src/components/ModalHost.tsx b/packages/ui/src/components/ModalHost.tsx new file mode 100644 index 0000000..c9454bf --- /dev/null +++ b/packages/ui/src/components/ModalHost.tsx @@ -0,0 +1,22 @@ +import { useRuntimeState } from "context/runtimeContext"; +import useProjectData from "hooks/useProjectData"; +import Modal from "./Modal"; +import { useBuilderMode } from "context/builderMode"; +import { ModalNode } from "types"; + +export default function ModalHost() { + const { activeModalId } = useRuntimeState(); + if (!activeModalId) return null; + + const targetNode = useProjectData(activeModalId) as ModalNode; + + if (!targetNode) return; + + return ( + + { + //모달의 하위 노드들 children으로 렌더링? + } + + ); +} diff --git a/packages/ui/src/hooks/useProjectData.tsx b/packages/ui/src/hooks/useProjectData.tsx new file mode 100644 index 0000000..6b407ba --- /dev/null +++ b/packages/ui/src/hooks/useProjectData.tsx @@ -0,0 +1,8 @@ +import { WcxNode } from "types"; + +export default function useProjectData(id: string) { + //DB에서 모달 id를 추출해 알맞는 모달 데이터 불러온다. + let nodeData: WcxNode | null; + //Id에 해당하는 모달 노드 데이터를 반환한다. + return nodeData; +} From 9e62a5f3943b2706f5846ef86c298989c88f4701 Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 24 Dec 2025 22:25:17 +0900 Subject: [PATCH 06/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EB=8D=94=EB=AF=B8?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=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/hooks/useProjectData.tsx | 30 +++++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/hooks/useProjectData.tsx b/packages/ui/src/hooks/useProjectData.tsx index 6b407ba..14f7aca 100644 --- a/packages/ui/src/hooks/useProjectData.tsx +++ b/packages/ui/src/hooks/useProjectData.tsx @@ -1,8 +1,26 @@ -import { WcxNode } from "types"; +// 임시 Mock 데이터 Hook +// 실제 DB 연동 전까지 데이터 구조를 테스트 용도로 사용합니다. -export default function useProjectData(id: string) { - //DB에서 모달 id를 추출해 알맞는 모달 데이터 불러온다. - let nodeData: WcxNode | null; - //Id에 해당하는 모달 노드 데이터를 반환한다. - return nodeData; +import { ModalNode } from "../types/nodes"; + + +//실제로는 id를 입력받으면 DB에서 해당하는 id의 노드데이터를 가져와서 반환하는 함수 +export function useProjectData(id: string | null) { + if (!id) return { modal: null }; + const modal: ModalNode = { + id: "modal-sample-1", + type: "Modal", + page_id: "global", + parent_id: null, + position: 0, + style: {}, + layout: { x: 0, y: 0, width: 400, height: 300, zIndex: 100 }, + props: { + alignment: "center", + overlayColor: "bg-black/50", + closeOnOverlayClick: true, + }, + }; + + return { modal }; } From 3c9f4e341b4899215d7f3ebe5082592dd9bf5977 Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 24 Dec 2025 22:47:28 +0900 Subject: [PATCH 07/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20modal=EB=85=B8?= =?UTF-8?q?=EB=93=9C=20=EB=A0=8C=EB=8D=94=EB=9F=AC=20=EC=97=AD=ED=95=A0=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 모달의 오버레이도 렌더링하던 역할을 분리함. 오직 modal노드만 렌더링 하도록 역할을 작게 나눔. --- packages/ui/src/components/Modal.tsx | 94 ++++++++++++---------------- 1 file changed, 39 insertions(+), 55 deletions(-) diff --git a/packages/ui/src/components/Modal.tsx b/packages/ui/src/components/Modal.tsx index 258a1d8..12217f5 100644 --- a/packages/ui/src/components/Modal.tsx +++ b/packages/ui/src/components/Modal.tsx @@ -1,9 +1,30 @@ -import { useBuilderMode } from "context/builderMode"; -import { useRuntimeState } from "context/runtimeContext"; import { ModalNode, NodeComponentProps } from "types"; import processNodeStyles from "utils/processNodeStyles"; +import { motion } from "framer-motion"; -//TODO-모달 IsOpen상태 관리를 어떻게 최적화 할 수 있을까? +const animationVariants = { + fade: { + hidden: { opacity: 0 }, + visible: { opacity: 1 }, + exit: { opacity: 0 }, + }, + "slide-up": { + hidden: { opacity: 0, y: 100 }, + visible: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: 100 }, + }, + "slide-right": { + hidden: { opacity: 0, x: 100 }, + visible: { opacity: 1, x: 0 }, + exit: { opacity: 0, x: 100 }, + }, + // 기본값 (Zoom In) + default: { + hidden: { opacity: 0, scale: 0.9 }, + visible: { opacity: 1, scale: 1 }, + exit: { opacity: 0, scale: 0.9 }, + }, +}; /** * @@ -14,67 +35,30 @@ import processNodeStyles from "utils/processNodeStyles"; */ export default function Modal({ node, - props, style, children, //모달안에 들어갈 버튼, 텍스트 등이 children으로 올 수 있습니다. }: NodeComponentProps) { - // Hooks & state - const { mode } = useBuilderMode(); const curNodeId = node.id; - const { - alignment, - overlayColor = "", - closeOnOverlayClick = "", - animation = "", - } = props; - - //Context를 통한 상태 구독 - //TODO-만약 context의 형태가 context API에서 zustand로 바뀐다면 해당 로직도 수정되야 합니다. - - - const { closeModal } = useRuntimeState(); - - // 위치 프리셋에 따른 CSS 클래스 매핑 - const alignmentStyles = { - center: "items-center justify-center", - top: "items-start justify-center pt-10", - bottom: "items-end justify-center pb-0", // 바텀 시트 스타일 - left: "items-center justify-start h-full", // 사이드바 - right: "items-center justify-end h-full", // 사이드바 - }; - - const positionClass = alignmentStyles[alignment] || alignmentStyles.center; + const animationType = node.props.animation || "default"; //스타일 변환 const nodeStyleObj = processNodeStyles(style); - //닫기 핸들러 - function closeHandler(e?: React.MouseEvent) { - if (mode === "editor") return; - e?.stopPropagation(); - closeModal(); - } - - function overlayClickHandler(e?: React.MouseEvent) { - if (closeOnOverlayClick === false) return; - closeHandler(e); - } - return ( -
overlayClickHandler(e)} + 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} + initial="hidden" + animate="visible" + exit="exit" + transition={{ duration: 0.2, ease: "easeOut" }} > - {/* 실제 모달 컨테이너 */} -
e.stopPropagation()} - className={`${style.root?.className || ""} pointer-events-auto bg-white shadow-2xl`} - style={nodeStyleObj.root} - > - {/* [Body] 자식 노드들(버튼, 이미지, 텍스트)이 여기에 렌더링 됨 */} -
{children}
-
-
+ {/* [Body] 자식 노드들(버튼, 이미지, 텍스트)이 여기에 렌더링 됨 */} +
{children}
+ ); } From 56c0473277f56a69e06ef00f5f530df34ee06493 Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 24 Dec 2025 22:48:55 +0900 Subject: [PATCH 08/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20modalHost=EC=97=AD?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 모달 노드의 오버레이를 렌더링 하도록 역할 수정. ModalHost가 DB에서 모달관련 데이터를 받아오면 모달 노드렌더러에게 props으로 전달하며 모달 노드 렌더럴을 호출한다. --- packages/ui/package.json | 3 ++ packages/ui/src/components/ModalHost.tsx | 60 +++++++++++++++++++----- pnpm-lock.yaml | 38 +++++++++++++++ 3 files changed, 90 insertions(+), 11 deletions(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index 1c42cde..2ef9172 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -16,5 +16,8 @@ "@types/react": "^19", "@types/react-dom": "^19", "typescript": "^5" + }, + "dependencies": { + "framer-motion": "^12.23.26" } } diff --git a/packages/ui/src/components/ModalHost.tsx b/packages/ui/src/components/ModalHost.tsx index c9454bf..063e661 100644 --- a/packages/ui/src/components/ModalHost.tsx +++ b/packages/ui/src/components/ModalHost.tsx @@ -1,22 +1,60 @@ import { useRuntimeState } from "context/runtimeContext"; -import useProjectData from "hooks/useProjectData"; +import { useProjectData } from "hooks/useProjectData"; import Modal from "./Modal"; -import { useBuilderMode } from "context/builderMode"; import { ModalNode } from "types"; +import { motion, AnimatePresence } from "framer-motion"; export default function ModalHost() { - const { activeModalId } = useRuntimeState(); - if (!activeModalId) return null; + //Context를 통한 상태 구독 + //TODO-만약 context의 형태가 context API에서 zustand로 바뀐다면 해당 로직도 수정되야 합니다. + const { activeModalId, closeModal } = useRuntimeState(); - const targetNode = useProjectData(activeModalId) as ModalNode; + //DB에서 관련 데이터 불러오기 + const { modal: targetNode } = useProjectData(activeModalId); - if (!targetNode) return; + // 닫기 핸들러 + const handleClose = () => { + closeModal(); + }; + + const handleOverlayClick = (closeOnOverlayClick?: boolean) => { + if (closeOnOverlayClick === false) return; + handleClose(); + }; + + // 위치 프리셋 + const alignmentStyles = { + center: "items-center justify-center", + top: "items-start justify-center pt-10", + bottom: "items-end justify-center pb-0", + left: "items-center justify-start h-full", + right: "items-center justify-end h-full", + }; return ( - - { - //모달의 하위 노드들 children으로 렌더링? - } - + + {activeModalId && targetNode && ( + + handleOverlayClick(targetNode.props.closeOnOverlayClick) + } + > +
e.stopPropagation()}> + + {/* Children */} + +
+
+ )} +
); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb355bc..40df467 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,6 +87,9 @@ importers: packages/ui: dependencies: + framer-motion: + specifier: ^12.23.26 + version: 12.23.26(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next: specifier: 15.5.6 version: 15.5.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -1203,6 +1206,20 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + framer-motion@12.23.26: + resolution: {integrity: sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -1604,6 +1621,12 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + motion-dom@12.23.23: + resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==} + + motion-utils@12.23.6: + resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -3347,6 +3370,15 @@ snapshots: dependencies: is-callable: 1.2.7 + framer-motion@12.23.26(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + motion-dom: 12.23.23 + motion-utils: 12.23.6 + tslib: 2.8.1 + optionalDependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -3731,6 +3763,12 @@ snapshots: minimist@1.2.8: {} + motion-dom@12.23.23: + dependencies: + motion-utils: 12.23.6 + + motion-utils@12.23.6: {} + mrmime@2.0.1: {} ms@2.1.3: {} From 2ab96b8899b8e7f978b9af0f402a45767eded5fb Mon Sep 17 00:00:00 2001 From: y-minion Date: Wed, 24 Dec 2025 23:45:24 +0900 Subject: [PATCH 09/14] =?UTF-8?q?=F0=9F=93=9D=20=EB=AA=A8=EB=8B=AC=20?= =?UTF-8?q?=EB=85=B8=EB=93=9C=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=84=A4?= =?UTF-8?q?=EA=B3=84=20=EB=A6=AC=EB=93=9C=EB=AF=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 팀원들과 공유할 수 있는 모달 노드 전용 설계 문서 작성 --- .../components/modal_system_architecture.md | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 packages/ui/src/components/modal_system_architecture.md diff --git a/packages/ui/src/components/modal_system_architecture.md b/packages/ui/src/components/modal_system_architecture.md new file mode 100644 index 0000000..1ff2e41 --- /dev/null +++ b/packages/ui/src/components/modal_system_architecture.md @@ -0,0 +1,214 @@ +# 🏗️ WebCreator-X: Modal System Architecture + +**Version:** 2.0.0 (Final) + +**Last Updated:** 2025.12.18 + +**Author:** WebCreator-X Core Team + +## 1. 개요 (Overview) + +본 문서는 웹 빌더 내에서 가장 복잡한 인터랙션 요소인 **모달(Modal) 시스템**의 설계 및 구현 명세를 정의합니다. +기존의 개별 상태 관리 방식의 비효율성을 개선하기 위해 **중앙 집중식 호스트(Centralized Host)** 패턴을 채택하였으며, 에디터에서의 작업 효율을 위해 **격리 편집 모드(Isolation Mode)**를 도입했습니다. + +### 🎯 핵심 목표 + +1. **성능 최적화:** 수십 개의 모달이 있어도 초기 로딩과 리렌더링 비용을 최소화한다. +2. **편집 편의성:** 에디터에서 모달이 작업 공간을 침범하지 않도록 별도 레이어에서 관리한다. +3. **연결성:** 버튼(Trigger)과 모달(Target)을 ID 참조(Reference) 방식으로 느슨하게 연결한다. + +--- + +## 2. 아키텍처 (Architecture) + +### 2.1 데이터 흐름도 (Data Flow) + +모든 모달의 제어는 `RuntimeContext`를 통해 단방향으로 흐릅니다. + +```mermaid +sequenceDiagram + participant User + participant Button as Button Node (Trigger) + participant Context as RuntimeContext (Store) + participant Host as ModalHost (Global Renderer) + participant Modal as Modal Content + + Note over User, Button: [Action] 1. 버튼 클릭 + User->>Button: Click Event + Button->>Button: useActionHandler() 실행 + Button->>Context: openModal("modal-login-ID") + + Note over Context, Host: [State Update] 2. 상태 변경 + Context->>Context: activeModalId = "modal-login-ID" + Context-->>Host: 변경 감지 (Re-render) + + Note over Host, Modal: [Rendering] 3. 화면 표시 + Host->>Host: Global Modal List에서 ID 검색 + Host->>Modal: 마운트 (Portal) + +``` + +### 2.2 핵심 패턴: Modal Host + +- **기존 방식 (Anti-Pattern):** 각 모달 컴포넌트가 개별적으로 `isOpen` 상태를 구독. (N개의 리스너) +- **최종 방식 (Best Practice):** 최상위 `ModalHost` 컴포넌트 **단 하나만** Context를 구독. 활성화된 모달 ID가 있을 때만 해당 콘텐츠를 동적으로 로드하여 렌더링. + +--- + +## 3. 데이터 스키마 (Data Schema) + +### 3.1 Runtime State (`RuntimeContext`) + +복잡한 객체 대신 **단순 문자열 ID** 하나만 관리합니다. + +```typescript +interface RuntimeContextType { + // 현재 열려있는 모달의 ID (없으면 null) + activeModalId: string | null; + + // 액션 함수 + openModal: (id: string) => void; + closeModal: () => void; +} +``` + +### 3.2 Node Data Structure + +DB의 `nodes` 테이블에 저장되는 데이터 구조입니다. + +#### A. Modal Node (Target) + +모달은 페이지(`page_id`)에 귀속되지 않고 전역(`Global`)으로 관리되는 것을 권장합니다. + +```typescript +interface ModalNode { + id: string; + type: "Modal"; + props: { + // ⭐️ 위치 프리셋: 마우스 드래그 대신 설정값으로 위치 결정 + alignment: "center" | "top" | "bottom" | "left" | "right"; + + // 스타일 옵션 + width?: string; + overlayColor?: string; // 예: "bg-black/50" + closeOnOverlayClick?: boolean; + + // 애니메이션 효과 + animation?: "fade" | "slide-up" | "slide-right"; + }; + children: WcxNode[]; // 모달 내부의 텍스트, 인풋, 버튼 등 +} +``` + +#### B. Button Node (Trigger) + +버튼은 모달을 직접 포함하지 않고 **ID만 참조**합니다. + +```typescript +interface ButtonNode { + type: "Button"; + props: { + text: "로그인"; + action: { + type: "modal"; // 동작 타입 + targetId: "modal-1234"; // 🔗 연결된 모달의 ID 참조 + }; + }; +} +``` + +--- + +## 4. 구현 상세 (Implementation Details) + +### 4.1 `RuntimeContext.tsx` + +상태 관리의 심장부입니다. + +```tsx +export function RuntimeProvider({ children }: { children: ReactNode }) { + // 초기값 null = 아무것도 안 열림 + const [activeModalId, setActiveModalId] = useState(null); + + const openModal = (id: string) => setActiveModalId(id); + const closeModal = () => setActiveModalId(null); + + return ( + + {children} + + ); +} +``` + +### 4.2 `ModalHost.tsx` + +실제 렌더링을 담당하는 총괄 매니저입니다. `layout.tsx` 최상단에 배치됩니다. + +```tsx +export function ModalHost() { + const { activeModalId, closeModal } = useRuntimeState(); + const { modals } = useProjectData(); // 전역 모달 리스트 Fetch + + // 1. 활성화된 모달이 없으면 렌더링 자체를 안 함 (성능 최적화) + if (!activeModalId) return null; + + // 2. ID에 맞는 모달 데이터 찾기 + const targetNode = modals.find((m) => m.id === activeModalId); + if (!targetNode) return null; + + return ( + // 3. Portal을 사용하여 DOM 최상위로 이동 (z-index 문제 해결) +
+
e.stopPropagation()}> + +
+
+ ); +} +``` + +--- + +## 5. UI/UX 전략 (Editor vs Live) + +### 5.1 격리 편집 모드 (Isolation Mode) + +에디터에서 모달을 편집할 때의 UX 표준입니다. + +1. **진입:** 사이드바 [모달 관리] 탭에서 모달 선택. +2. **화면 변화:** + +- 기존 캔버스(페이지 노드들)는 `Dimmed` (어둡게) 처리되고 선택 불가능 상태가 됨. +- 선택된 모달만 화면에 렌더링됨. + +3. **위치 설정:** 마우스 드래그 불가. 우측 속성 패널의 **[위치 프리셋]** 버튼(상/하/좌/우/중앙)으로 위치 조정. +4. **복귀:** 상단 [편집 종료] 버튼 클릭 시 모달이 사라지고 원래 페이지 편집 화면으로 복귀. + +### 5.2 위치 프리셋 (Alignment Presets) + +사용자가 반응형 CSS를 몰라도 완벽한 위치를 잡을 수 있게 합니다. + +| 프리셋 이름 | 적용 CSS (Tailwind) | 용도 | +| ----------- | ----------------------------------- | ------------------------ | +| **Center** | `items-center justify-center` | 알림창, 로그인 폼 | +| **Top** | `items-start justify-center pt-10` | 토스트 메시지, 상단 공지 | +| **Bottom** | `items-end justify-center pb-0` | 모바일 바텀 시트 | +| **Left** | `items-center justify-start h-full` | 사이드바 메뉴 (Drawer) | +| **Right** | `items-center justify-end h-full` | 장바구니, 필터 | + +--- + +## 6. 개발 로드맵 (Roadmap) + +1. **Phase 1 (Core):** `RuntimeContext`, `ModalHost`, `ModalRenderer` 구현. 기본 `Center` 정렬만 지원. +2. **Phase 2 (Editor UX):** 사이드바 모달 리스트 UI 및 격리 편집 모드(Dimmed 처리) 구현. +3. **Phase 3 (Presets):** Top/Bottom/Side 정렬 및 슬라이드 애니메이션 추가. +4. **Phase 4 (Templates):** 빈 모달 대신 '로그인', '뉴스레터' 등 템플릿 제공. + +--- + +이 문서는 WebCreator-X 팀의 합의된 모달 시스템 설계이며, 향후 기능 확장 시 본 문서를 기준으로 업데이트합니다. From 440ba09c1582ab7be824001dbd3361b56bc77aa9 Mon Sep 17 00:00:00 2001 From: y-minion Date: Thu, 25 Dec 2025 12:17:54 +0900 Subject: [PATCH 10/14] =?UTF-8?q?=E2=9C=A8=EB=9D=BC=EC=9D=B4=EB=B8=8C=20?= =?UTF-8?q?=EB=AA=A8=EB=93=9C=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EB=85=B8=EB=93=9C=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=9F=AC=20=EB=9E=98=ED=8D=BC=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=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/LiveModeWrapper.tsx | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 packages/ui/src/core/LiveModeWrapper.tsx diff --git a/packages/ui/src/core/LiveModeWrapper.tsx b/packages/ui/src/core/LiveModeWrapper.tsx new file mode 100644 index 0000000..038836b --- /dev/null +++ b/packages/ui/src/core/LiveModeWrapper.tsx @@ -0,0 +1,21 @@ +import { BaseNode } from "types"; + +//라이브 모드에서 사용되는 노드 렌더러 래퍼 컴포넌트입니다. +interface props { + children: React.ReactNode; + node: BaseNode; +} + +export default function LiveModeWrapper({ children, node }: props) { + const { x, y, width, height, zIndex } = node.layout; + + const wrapperStyle: React.CSSProperties = { + position: "absolute", + left: x, + top: y, + width, + height, + zIndex, + }; + return
{children}
; +} From 7e9888a9f028dc5bf3e9d4f4b87a1305373f9798 Mon Sep 17 00:00:00 2001 From: y-minion Date: Thu, 25 Dec 2025 13:25:54 +0900 Subject: [PATCH 11/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EC=97=90=EB=94=94?= =?UTF-8?q?=ED=84=B0=20=EB=AA=A8=EB=93=9C=EC=9D=98=20=EB=85=B8=EB=93=9C=20?= =?UTF-8?q?=EB=9E=98=ED=8D=BC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=9D=B4=EC=9B=90=ED=99=94=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존에 라이브/에디터 모드에서 모두 사용되면 래퍼 컴포넌트를 에디터 모드에서만 사용되도록 역할 분리 완료. 라이브 모드에서는 LiveModeWrapper래퍼 컴포넌트를 사용하도록 분리 완료. --- packages/ui/src/core/DraggableNodeWrapper.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/ui/src/core/DraggableNodeWrapper.tsx b/packages/ui/src/core/DraggableNodeWrapper.tsx index 9b149b7..b896ee4 100644 --- a/packages/ui/src/core/DraggableNodeWrapper.tsx +++ b/packages/ui/src/core/DraggableNodeWrapper.tsx @@ -1,5 +1,4 @@ -import { useBuilderMode } from "context/builderMode"; -import { CSSProperties } from "react"; +//에디터 모드전용 노드 렌더러 래퍼 컴포넌트 import { BaseNode } from "types"; interface WrapperProps { @@ -15,8 +14,7 @@ export default function DraggableNodeWrapper({ isSelected, onDragStart, }: WrapperProps) { - const { mode } = useBuilderMode(); - const wrapperStyle: CSSProperties = { + const wrapperStyle: React.CSSProperties = { position: "absolute", // 핵심: 캔버스 내에서 자유 배치 left: node["layout"].x, top: node["layout"].y, @@ -25,14 +23,14 @@ export default function DraggableNodeWrapper({ zIndex: node["layout"].zIndex, // 선택되었을 때 시각적 피드백 (테두리 등) - outline: mode === "editor" && isSelected ? "2px solid blue" : "none", - cursor: mode === "editor" ? "move" : "default", + outline: isSelected ? "2px solid blue" : "none", + cursor: "move", }; return (
mode === "editor" && onDragStart(e, node.id)} + onMouseDown={(e) => onDragStart(e, node.id)} className="" //TODO - 필요시 클래스네임 추가(dnd상황에서 스타일 강조 등) > {/* 실제 컴포넌트(Hero 등)는 이 안에 렌더링됨 */} @@ -40,7 +38,7 @@ export default function DraggableNodeWrapper({ {/* 리사이즈 핸들 등은 에디터 모드에서만 오버레이로 표시 */} {/* TODO-추후 호버시 리사이즈 핸들 렌더링 하도록 수정 필요! */} - {isSelected && mode === "editor" && ( + {isSelected && ( <>
{/* TODO-기타 리사이즈 핸들들 렌더링은 이곳에서 ... */} From 58f3d55b328d5557f0f2353137f4f9cf6ac428fd Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 29 Dec 2025 17:34:25 +0900 Subject: [PATCH 12/14] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20=EB=A6=AC=EC=95=A1?= =?UTF-8?q?=ED=8A=B8=20=EB=B3=B4=EC=95=88=20=EC=9D=B4=EC=8A=88=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=B4=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=97=85?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/editor/package.json | 2 +- packages/ui/package.json | 2 +- pnpm-lock.yaml | 86 ++++++++++++++++++++-------------------- 3 files changed, 45 insertions(+), 45 deletions(-) diff --git a/apps/editor/package.json b/apps/editor/package.json index 355ba28..e2d9611 100644 --- a/apps/editor/package.json +++ b/apps/editor/package.json @@ -14,7 +14,7 @@ "@repo/ui": "workspace:*", "@tanstack/react-query": "^5.90.8", "json-server": "^1.0.0-beta.3", - "next": "15.5.6", + "next": "15.5.9", "react": "19.1.0", "react-dom": "19.1.0", "zustand": "^5.0.8" diff --git a/packages/ui/package.json b/packages/ui/package.json index 2ef9172..45b7be5 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -8,7 +8,7 @@ "type-check": "tsc --noEmit" }, "peerDependencies": { - "next": "15.5.6", + "next": "15.5.9", "react": "19.1.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40df467..576aab3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: ^1.0.0-beta.3 version: 1.0.0-beta.3 next: - specifier: 15.5.6 - version: 15.5.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: 15.5.9 + version: 15.5.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: specifier: 19.1.0 version: 19.1.0 @@ -91,8 +91,8 @@ importers: specifier: ^12.23.26 version: 12.23.26(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next: - specifier: 15.5.6 - version: 15.5.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: 15.5.9 + version: 15.5.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: specifier: 19.1.0 version: 19.1.0 @@ -335,56 +335,56 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@next/env@15.5.6': - resolution: {integrity: sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q==} + '@next/env@15.5.9': + resolution: {integrity: sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==} '@next/eslint-plugin-next@15.5.6': resolution: {integrity: sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ==} - '@next/swc-darwin-arm64@15.5.6': - resolution: {integrity: sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg==} + '@next/swc-darwin-arm64@15.5.7': + resolution: {integrity: sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.5.6': - resolution: {integrity: sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA==} + '@next/swc-darwin-x64@15.5.7': + resolution: {integrity: sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.5.6': - resolution: {integrity: sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg==} + '@next/swc-linux-arm64-gnu@15.5.7': + resolution: {integrity: sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.5.6': - resolution: {integrity: sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w==} + '@next/swc-linux-arm64-musl@15.5.7': + resolution: {integrity: sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.5.6': - resolution: {integrity: sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA==} + '@next/swc-linux-x64-gnu@15.5.7': + resolution: {integrity: sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.5.6': - resolution: {integrity: sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ==} + '@next/swc-linux-x64-musl@15.5.7': + resolution: {integrity: sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.5.6': - resolution: {integrity: sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg==} + '@next/swc-win32-arm64-msvc@15.5.7': + resolution: {integrity: sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.5.6': - resolution: {integrity: sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ==} + '@next/swc-win32-x64-msvc@15.5.7': + resolution: {integrity: sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1651,8 +1651,8 @@ packages: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} - next@15.5.6: - resolution: {integrity: sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==} + next@15.5.9: + resolution: {integrity: sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -2376,34 +2376,34 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@next/env@15.5.6': {} + '@next/env@15.5.9': {} '@next/eslint-plugin-next@15.5.6': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.5.6': + '@next/swc-darwin-arm64@15.5.7': optional: true - '@next/swc-darwin-x64@15.5.6': + '@next/swc-darwin-x64@15.5.7': optional: true - '@next/swc-linux-arm64-gnu@15.5.6': + '@next/swc-linux-arm64-gnu@15.5.7': optional: true - '@next/swc-linux-arm64-musl@15.5.6': + '@next/swc-linux-arm64-musl@15.5.7': optional: true - '@next/swc-linux-x64-gnu@15.5.6': + '@next/swc-linux-x64-gnu@15.5.7': optional: true - '@next/swc-linux-x64-musl@15.5.6': + '@next/swc-linux-x64-musl@15.5.7': optional: true - '@next/swc-win32-arm64-msvc@15.5.6': + '@next/swc-win32-arm64-msvc@15.5.7': optional: true - '@next/swc-win32-x64-msvc@15.5.6': + '@next/swc-win32-x64-msvc@15.5.7': optional: true '@nodelib/fs.scandir@2.1.5': @@ -3781,9 +3781,9 @@ snapshots: negotiator@0.6.4: {} - next@15.5.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + next@15.5.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - '@next/env': 15.5.6 + '@next/env': 15.5.9 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001757 postcss: 8.4.31 @@ -3791,14 +3791,14 @@ snapshots: react-dom: 19.1.0(react@19.1.0) styled-jsx: 5.1.6(react@19.1.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.5.6 - '@next/swc-darwin-x64': 15.5.6 - '@next/swc-linux-arm64-gnu': 15.5.6 - '@next/swc-linux-arm64-musl': 15.5.6 - '@next/swc-linux-x64-gnu': 15.5.6 - '@next/swc-linux-x64-musl': 15.5.6 - '@next/swc-win32-arm64-msvc': 15.5.6 - '@next/swc-win32-x64-msvc': 15.5.6 + '@next/swc-darwin-arm64': 15.5.7 + '@next/swc-darwin-x64': 15.5.7 + '@next/swc-linux-arm64-gnu': 15.5.7 + '@next/swc-linux-arm64-musl': 15.5.7 + '@next/swc-linux-x64-gnu': 15.5.7 + '@next/swc-linux-x64-musl': 15.5.7 + '@next/swc-win32-arm64-msvc': 15.5.7 + '@next/swc-win32-x64-msvc': 15.5.7 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' From c7303acb38c320dfbe7d650cfd986da8b2b853c9 Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 29 Dec 2025 17:37:22 +0900 Subject: [PATCH 13/14] =?UTF-8?q?=E2=9C=A8=20=EC=97=90=EB=94=94=ED=84=B0?= =?UTF-8?q?=20=EB=AA=A8=EB=93=9C=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EB=90=98=EB=8A=94=20useEditorStore=20=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EC=96=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 에디터 섹션에서 노드를 클릭할때 사이드바에서 선택된 노드를 알 수 있게 해줌. 스토어 내부의 액션들을 내무에서 커스텀 훅으로 따로export해서 나중에 스토어가 커지거나 변경되도 내부에서만 변경할 수 있도록 구현 완료. --- apps/editor/src/stores/useEditorStore.ts | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 apps/editor/src/stores/useEditorStore.ts diff --git a/apps/editor/src/stores/useEditorStore.ts b/apps/editor/src/stores/useEditorStore.ts new file mode 100644 index 0000000..f83dd1d --- /dev/null +++ b/apps/editor/src/stores/useEditorStore.ts @@ -0,0 +1,45 @@ +import { create } from "zustand"; +import { combine, devtools } from "zustand/middleware"; +import { immer } from "zustand/middleware/immer"; + +const useEditorStore = create( + devtools( + immer( + combine( + { + selectedNodeId: null as string | null, + }, + (set) => ({ + selectNode(id: string) { + set( + (state) => { + state.selectedNodeId = id; + }, + false, + "editorStore/selectNode", + ); + }, + clearNode() { + set( + (state) => { + state.selectedNodeId = null; + }, + false, + "editorStore/clearNode", + ); + }, + }), + ), + ), + { + name: "editorStore", + }, + ), +); + +export const useSelectedNodeId = () => + useEditorStore((store) => store.selectedNodeId); + +export const useSelectNode = () => useEditorStore((store) => store.selectNode); + +export const useClearNode = () => useEditorStore((store) => store.clearNode); From e4ec0b1463a231a8adc81c105c734c8f381d36d3 Mon Sep 17 00:00:00 2001 From: y-minion Date: Mon, 29 Dec 2025 18:07:38 +0900 Subject: [PATCH 14/14] =?UTF-8?q?=E2=9C=A8=20=EB=AA=A8=EB=93=A0=20?= =?UTF-8?q?=EB=AA=A8=EB=93=9C=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EB=85=B8=EB=93=9C=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=9F=AC=20=EB=9E=98=ED=8D=BC=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=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 두가지 모드에서 사용되는 노드 렌더러 래퍼컴포넌트 구현완료. 노드 렌더러는 항상 래퍼 컴포넌트를 100%채우도록 렌더링 되기 때문에 노드의 레이아웃은 현재 래퍼 컴포넌트가 담당한다. --- packages/ui/src/core/BaseLayoutWrapper.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 packages/ui/src/core/BaseLayoutWrapper.tsx diff --git a/packages/ui/src/core/BaseLayoutWrapper.tsx b/packages/ui/src/core/BaseLayoutWrapper.tsx new file mode 100644 index 0000000..4946a1e --- /dev/null +++ b/packages/ui/src/core/BaseLayoutWrapper.tsx @@ -0,0 +1,15 @@ +import { BaseNode } from "types"; + +interface Props { + children: React.ReactNode; + node: BaseNode; +} + +export default function BaseLayoutNodeWrapper({ children, node }: Props) { + const { x: top, y: left, width, height, zIndex } = node.layout; + return ( +
+ {children} +
+ ); +}