From b9ab58fde1ef6cbcd3db642701ceb44dc602d2b6 Mon Sep 17 00:00:00 2001 From: "DESKTOP-4N4R3LH\\jqk17" Date: Sun, 8 Feb 2026 23:46:11 +0900 Subject: [PATCH 01/15] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=EB=B0=94=20=EA=B8=B0=EB=B3=B8=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8(Atom)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../right-sidebar/ui/atoms/ColorInput.tsx | 46 +++++++++++++++++++ .../right-sidebar/ui/atoms/ControlRow.tsx | 20 ++++++++ .../right-sidebar/ui/atoms/NumberInput.tsx | 21 +++++++++ .../right-sidebar/ui/atoms/SelectInput.tsx | 35 ++++++++++++++ .../right-sidebar/ui/atoms/SidebarItem.tsx | 20 ++++++++ .../right-sidebar/ui/atoms/TextInput.tsx | 23 ++++++++++ 6 files changed, 165 insertions(+) create mode 100644 apps/editor/src/widgets/right-sidebar/ui/atoms/ColorInput.tsx create mode 100644 apps/editor/src/widgets/right-sidebar/ui/atoms/ControlRow.tsx create mode 100644 apps/editor/src/widgets/right-sidebar/ui/atoms/NumberInput.tsx create mode 100644 apps/editor/src/widgets/right-sidebar/ui/atoms/SelectInput.tsx create mode 100644 apps/editor/src/widgets/right-sidebar/ui/atoms/SidebarItem.tsx create mode 100644 apps/editor/src/widgets/right-sidebar/ui/atoms/TextInput.tsx diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/ColorInput.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/ColorInput.tsx new file mode 100644 index 0000000..2147e34 --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/ColorInput.tsx @@ -0,0 +1,46 @@ +"use client"; + +interface ColorInputProps { + id?: string; + name?: string; + value: string; + onChange: (val: string) => void; +} + +export default function ColorInput({ id, name, value, onChange }: ColorInputProps) { + return ( +
+ {/* Color Swatch / Picker Trigger */} +
+ onChange(e.target.value)} + className="absolute inset-0 opacity-0 cursor-pointer w-full h-full" + /> +
+
+ + {/* Hex Text */} + # + { + const hex = e.target.value.replace(/[^0-9A-Fa-f]/g, ""); + if (hex.length <= 6) { + onChange(`#${hex}`); + } + }} + className="w-full bg-transparent font-inter font-normal text-[14px] leading-none text-zinc-900 outline-none uppercase" + maxLength={6} + /> +
+ ); +} diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/ControlRow.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/ControlRow.tsx new file mode 100644 index 0000000..2443cc1 --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/ControlRow.tsx @@ -0,0 +1,20 @@ +"use client"; + +import React from "react"; + +interface ControlRowProps { + children: React.ReactNode; +} + +/** + * 두 개 이상의 컨트롤을 한 줄에 배치하기 위한 그리드 레이아웃 헬퍼 + */ +export default function ControlRow({ children }: ControlRowProps) { + return ( +
+ {React.Children.map(children, (child) => ( +
{child}
+ ))} +
+ ); +} diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/NumberInput.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/NumberInput.tsx new file mode 100644 index 0000000..475da6d --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/NumberInput.tsx @@ -0,0 +1,21 @@ +"use client"; + +interface NumberInputProps { + id?: string; + name?: string; + value: number; + onChange: (val: number) => void; +} + +export default function NumberInput({ id, name, value, onChange }: NumberInputProps) { + return ( + onChange(Number(e.target.value))} + className="flex w-full h-[33px] items-center px-3 py-2 rounded-[6px] border border-[#E4E4E7] bg-white font-inter font-normal text-[14px] leading-none text-zinc-900 outline-none focus:border-zinc-400 transition-colors [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + /> + ); +} diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/SelectInput.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/SelectInput.tsx new file mode 100644 index 0000000..16327f2 --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/SelectInput.tsx @@ -0,0 +1,35 @@ +"use client"; + +interface SelectInputProps { + id?: string; + name?: string; + value: string; + options: { label: string; value: string }[]; + onChange: (val: string) => void; +} + +export default function SelectInput({ id, name, value, options, onChange }: SelectInputProps) { + return ( +
+ + {/* 커스텀 화살표 아이콘 (간단한 구현) */} +
+ + + +
+
+ ); +} diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/SidebarItem.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/SidebarItem.tsx new file mode 100644 index 0000000..725aa1e --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/SidebarItem.tsx @@ -0,0 +1,20 @@ +"use client"; + +interface SidebarItemProps { + label: string; + children: React.ReactNode; +} + +/** + * 모든 사이드바 입력 요소의 공통 래퍼 + */ +export default function SidebarItem({ label, children }: SidebarItemProps) { + return ( +
+ + {label} + + {children} +
+ ); +} diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/TextInput.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/TextInput.tsx new file mode 100644 index 0000000..6c30e80 --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/TextInput.tsx @@ -0,0 +1,23 @@ +"use client"; + +interface TextInputProps { + id?: string; + name?: string; + value: string; + onChange: (val: string) => void; + placeholder?: string; +} + +export default function TextInput({ id, name, value, onChange, placeholder }: TextInputProps) { + return ( + onChange(e.target.value)} + placeholder={placeholder} + className="flex w-full h-[33px] items-center px-3 py-2 rounded-[6px] border border-[#E4E4E7] bg-white font-inter font-normal text-[14px] leading-none text-zinc-900 placeholder:text-[#A1A1AA] outline-none focus:border-zinc-400 transition-colors" + /> + ); +} From 10e06f2a03b740ed2a67216b5da8da45ebdda48a Mon Sep 17 00:00:00 2001 From: "DESKTOP-4N4R3LH\\jqk17" Date: Mon, 9 Feb 2026 00:10:03 +0900 Subject: [PATCH 02/15] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A0=95=EB=A0=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B0=A9=ED=96=A5=20=EC=84=A0=ED=83=9D=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=ED=8A=B9=EC=88=98=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8(Atom)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../right-sidebar/ui/atoms/AlignPicker.tsx | 64 +++++++++++++++++++ .../ui/atoms/DirectionToggle.tsx | 42 ++++++++++++ .../ui/atoms/ToggleButtonGroup.tsx | 45 +++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 apps/editor/src/widgets/right-sidebar/ui/atoms/AlignPicker.tsx create mode 100644 apps/editor/src/widgets/right-sidebar/ui/atoms/DirectionToggle.tsx create mode 100644 apps/editor/src/widgets/right-sidebar/ui/atoms/ToggleButtonGroup.tsx diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/AlignPicker.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/AlignPicker.tsx new file mode 100644 index 0000000..dc9d679 --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/AlignPicker.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { cn } from "@repo/utils"; +import { + // 수평 정렬용 (교차축이 수평일 때 - Column 모드) + AlignStartVertical, + AlignCenterVertical, + AlignEndVertical, + // 수직 정렬용 (교차축이 수직일 때 - Row 모드) + AlignStartHorizontal, + AlignCenterHorizontal, + AlignEndHorizontal +} from "lucide-react"; + +interface AlignPickerProps { + direction: "row" | "column"; + value: string; + onChange: (val: string) => void; +} + +export default function AlignPicker({ direction, value, onChange }: AlignPickerProps) { + // 교차축(Align)의 방향 결정 + // Row(가로)일 때 교차축은 세로 정렬 아이콘이 필요함 + // Column(세로)일 때 교차축은 가로 정렬 아이콘이 필요함 + const isDirectionRow = direction === "row"; + + const options = isDirectionRow + ? [ + { icon: AlignStartHorizontal, value: "flex-start", label: "Top" }, + { icon: AlignCenterHorizontal, value: "center", label: "Middle" }, + { icon: AlignEndHorizontal, value: "flex-end", label: "Bottom" }, + ] + : [ + { icon: AlignStartVertical, value: "flex-start", label: "Left" }, + { icon: AlignCenterVertical, value: "center", label: "Center" }, + { icon: AlignEndVertical, value: "flex-end", label: "Right" }, + ]; + + return ( +
+ {options.map((opt) => { + const Icon = opt.icon; + const isActive = value === opt.value; + + return ( + + ); + })} +
+ ); +} diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/DirectionToggle.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/DirectionToggle.tsx new file mode 100644 index 0000000..7b0b5cc --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/DirectionToggle.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { cn } from "@repo/utils"; +import { MoveHorizontal, MoveVertical } from "lucide-react"; + +interface DirectionToggleProps { + value: "row" | "column"; + onChange: (val: "row" | "column") => void; +} + +export default function DirectionToggle({ value, onChange }: DirectionToggleProps) { + return ( +
+ + +
+ ); +} diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/ToggleButtonGroup.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/ToggleButtonGroup.tsx new file mode 100644 index 0000000..01472d6 --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/ToggleButtonGroup.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { cn } from "@repo/utils"; + +interface ToggleOption { + label: string; + value: string; +} + +interface ToggleButtonGroupProps { + options: ToggleOption[]; + value: string | string[]; + onChange: (val: string) => void; +} + +export default function ToggleButtonGroup({ + options, + value, + onChange, +}: ToggleButtonGroupProps) { + return ( +
+ {options.map((opt) => { + const isActive = Array.isArray(value) + ? value.includes(opt.value) + : value === opt.value; + + return ( + + ); + })} +
+ ); +} From 0cab0f87415f0f298ebb0b7a352d553199e59e02 Mon Sep 17 00:00:00 2001 From: "DESKTOP-4N4R3LH\\jqk17" Date: Mon, 9 Feb 2026 00:10:38 +0900 Subject: [PATCH 03/15] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=EC=84=A4=EC=A0=95=20=ED=8C=A8=EB=84=90=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../right-sidebar/ui/panels/LayoutPanel.tsx | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 apps/editor/src/widgets/right-sidebar/ui/panels/LayoutPanel.tsx diff --git a/apps/editor/src/widgets/right-sidebar/ui/panels/LayoutPanel.tsx b/apps/editor/src/widgets/right-sidebar/ui/panels/LayoutPanel.tsx new file mode 100644 index 0000000..e8b741c --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/panels/LayoutPanel.tsx @@ -0,0 +1,108 @@ +import { WcxNode } from "@repo/ui/types/nodes"; +import { useUpdateNode, useUpdateNodeLayout } from "@/stores/useEditorStore"; +import SidebarItem from "../atoms/SidebarItem"; +import NumberInput from "../atoms/NumberInput"; +import DirectionToggle from "../atoms/DirectionToggle"; +import AlignPicker from "../atoms/AlignPicker"; +import SelectInput from "../atoms/SelectInput"; +import ToggleButtonGroup from "../atoms/ToggleButtonGroup"; + +interface LayoutPanelProps { + node: WcxNode; +} + +export default function LayoutPanel({ node }: LayoutPanelProps) { + const updateLayout = useUpdateNodeLayout(); + const updateNode = useUpdateNode(); + + const handleStyleChange = (key: string, value: any) => { + updateNode(node.id, { style: { ...node.style, [key]: value } }); + }; + + const direction = (node.style.flexDirection as "row" | "column") || "row"; + + return ( +
+ {/* 1. Type (Stack / Grid) - 이미지의 Type 섹션 */} + + { }} // 추후 확장 가능성 + /> + + + {/* 2. Direction - 이미지의 Direction 섹션 */} + + handleStyleChange("flexDirection", val)} + /> + + + {/* 3. Distribute (Justify Content) - 이미지의 Distribute (Select) 섹션 */} + + handleStyleChange("justifyContent", val)} + /> + + + {/* 4. Align (Align Items) - 이미지의 Align (Icons) 섹션 */} + + handleStyleChange("alignItems", val)} + /> + + + {/* 5. Wrap & Gap & Padding - 추가적인 유용한 설정들 */} +
+
+ + handleStyleChange("gap", `${val}px`)} + /> + + + handleStyleChange("padding", `${val}px`)} + /> + +
+
+ + {/* 6. Dimensions (Width & Height) */} +
+
+ + updateLayout(node.id, { width: val })} + /> + + + updateLayout(node.id, { height: val })} + /> + +
+
+
+ ); +} From 4d897971eea1a9edb39c324a12b9171e79efb967 Mon Sep 17 00:00:00 2001 From: "DESKTOP-4N4R3LH\\jqk17" Date: Mon, 9 Feb 2026 00:11:06 +0900 Subject: [PATCH 04/15] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=85=8D=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=84=A4=EC=A0=95=20=ED=8C=A8=EB=84=90=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 --- .../right-sidebar/ui/panels/TextPanel.tsx | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 apps/editor/src/widgets/right-sidebar/ui/panels/TextPanel.tsx diff --git a/apps/editor/src/widgets/right-sidebar/ui/panels/TextPanel.tsx b/apps/editor/src/widgets/right-sidebar/ui/panels/TextPanel.tsx new file mode 100644 index 0000000..88b57e3 --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/panels/TextPanel.tsx @@ -0,0 +1,161 @@ +"use client"; + +import { TextNode } from "@repo/ui/types/nodes"; +import { useUpdateNode } from "@/stores/useEditorStore"; +import SidebarItem from "../atoms/SidebarItem"; +import TextInput from "../atoms/TextInput"; +import SelectInput from "../atoms/SelectInput"; +import NumberInput from "../atoms/NumberInput"; +import ColorInput from "../atoms/ColorInput"; +import ToggleButtonGroup from "../atoms/ToggleButtonGroup"; +import ControlRow from "../atoms/ControlRow"; +import AlignPicker from "../atoms/AlignPicker"; + +interface TextPanelProps { + node: TextNode; +} + +export default function TextPanel({ node }: TextPanelProps) { + const updateNode = useUpdateNode(); + + const handlePropChange = (key: string, value: any) => { + updateNode(node.id, { props: { ...node.props, [key]: value } }); + }; + + const handleStyleChange = (key: string, value: any) => { + updateNode(node.id, { style: { ...node.style, [key]: value } }); + }; + + return ( +
+ {/* 1. Text Content - Added as per request */} + + handlePropChange("text", val)} + placeholder="Enter text..." + /> + + + {/* 2. Font Family */} + + handleStyleChange("fontFamily", val)} + /> + + + {/* 3. Size & Weight */} + + + handleStyleChange("fontSize", `${val}px`)} + /> + + + handleStyleChange("fontWeight", val)} + /> + + + + {/* 4. Color */} + + handleStyleChange("color", val)} + /> + + + {/* 5. Style (Italic, Underline, Strike) */} + + v !== "" && v !== "none")} + options={[ + { label: "I", value: "italic" }, + { label: "U", value: "underline" }, + { label: "S", value: "line-through" }, + ]} + onChange={(val) => { + if (val === "italic") { + handleStyleChange("fontStyle", node.style.fontStyle === "italic" ? "normal" : "italic"); + } else { + handleStyleChange("textDecoration", node.style.textDecoration === val ? "none" : val); + } + }} + /> + + + {/* 6. Alignment (Horizontal & Vertical) */} + + handleStyleChange("justifyContent", val)} + /> + + + + handleStyleChange("alignItems", val)} + /> + + + {/* 7. Line Height & Letter Spacing */} + + + handleStyleChange("lineHeight", `${val}%`)} + /> + + + handleStyleChange("letterSpacing", `${val}px`)} + /> + + + + {/* 8. Heading Level */} + + handlePropChange("level", val)} + /> + +
+ ); +} From 49b5872c46f007da53382a20c32255cc56128617 Mon Sep 17 00:00:00 2001 From: "DESKTOP-4N4R3LH\\jqk17" Date: Mon, 9 Feb 2026 00:11:17 +0900 Subject: [PATCH 05/15] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=B0=8F=20=EB=B2=84=ED=8A=BC=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=ED=8C=A8=EB=84=90=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../right-sidebar/ui/panels/ButtonPanel.tsx | 40 +++++++++++++++++++ .../right-sidebar/ui/panels/ImagePanel.tsx | 30 ++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 apps/editor/src/widgets/right-sidebar/ui/panels/ButtonPanel.tsx create mode 100644 apps/editor/src/widgets/right-sidebar/ui/panels/ImagePanel.tsx diff --git a/apps/editor/src/widgets/right-sidebar/ui/panels/ButtonPanel.tsx b/apps/editor/src/widgets/right-sidebar/ui/panels/ButtonPanel.tsx new file mode 100644 index 0000000..e035af1 --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/panels/ButtonPanel.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { ButtonNode } from "@repo/ui/types/nodes"; +import { useUpdateNode } from "@/stores/useEditorStore"; +import SidebarItem from "../atoms/SidebarItem"; +import TextInput from "../atoms/TextInput"; +import LayoutPanel from "./LayoutPanel"; + +interface ButtonPanelProps { + node: ButtonNode; +} + +export default function ButtonPanel({ node }: ButtonPanelProps) { + const updateNode = useUpdateNode(); + + const handlePropChange = (key: string, value: any) => { + updateNode(node.id, { props: { ...node.props, [key]: value } }); + }; + + return ( +
+ {/* Label Section */} + + handlePropChange("text", val)} + placeholder="Enter button text..." + /> + + + {/* Common Layout Section */} + + + {/* Button Specific Sections (State, Hover, Trigger etc.) */} +
+ Button specialized settings (Hover, Trigger) will be added here. +
+
+ ); +} diff --git a/apps/editor/src/widgets/right-sidebar/ui/panels/ImagePanel.tsx b/apps/editor/src/widgets/right-sidebar/ui/panels/ImagePanel.tsx new file mode 100644 index 0000000..39cd917 --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/panels/ImagePanel.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { WcxNode } from "@repo/ui/types/nodes"; +import { useUpdateNode } from "@/stores/useEditorStore"; +import SidebarItem from "../atoms/SidebarItem"; +import TextInput from "../atoms/TextInput"; +import LayoutPanel from "./LayoutPanel"; + +interface ImagePanelProps { + node: WcxNode; +} + +export default function ImagePanel({ node }: ImagePanelProps) { + const updateNode = useUpdateNode(); + + return ( +
+ + + updateNode(node.id, { props: { ...node.props, alt: val } }) + } + placeholder="Image description..." + /> + + +
+ ); +} From e40b747852f41e8fc7a6636e0fbe079f9255f51a Mon Sep 17 00:00:00 2001 From: "DESKTOP-4N4R3LH\\jqk17" Date: Mon, 9 Feb 2026 00:11:31 +0900 Subject: [PATCH 06/15] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=9A=B0=EC=B8=A1=20?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=9C=84=EC=A0=AF=20=EB=B0=8F=20=EC=97=94=ED=8A=B8=EB=A6=AC=20?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=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 --- .../editor/src/widgets/right-sidebar/index.ts | 1 + .../widgets/right-sidebar/ui/RightSidebar.tsx | 52 +++++++++++++++++++ .../right-sidebar/ui/SettingsTitle.tsx | 13 +++++ 3 files changed, 66 insertions(+) create mode 100644 apps/editor/src/widgets/right-sidebar/index.ts create mode 100644 apps/editor/src/widgets/right-sidebar/ui/RightSidebar.tsx create mode 100644 apps/editor/src/widgets/right-sidebar/ui/SettingsTitle.tsx diff --git a/apps/editor/src/widgets/right-sidebar/index.ts b/apps/editor/src/widgets/right-sidebar/index.ts new file mode 100644 index 0000000..ef87bf0 --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/index.ts @@ -0,0 +1 @@ +export { default as RightSidebar } from "./ui/RightSidebar"; diff --git a/apps/editor/src/widgets/right-sidebar/ui/RightSidebar.tsx b/apps/editor/src/widgets/right-sidebar/ui/RightSidebar.tsx new file mode 100644 index 0000000..6a7f590 --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/RightSidebar.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { useCurNodes, useSelectedNodeId } from "@/stores/useEditorStore"; +import SettingsTitle from "./SettingsTitle"; +import TextPanel from "./panels/TextPanel"; +import ButtonPanel from "./panels/ButtonPanel"; +import ImagePanel from "./panels/ImagePanel"; +import LayoutPanel from "./panels/LayoutPanel"; +import { WcxNode } from "@repo/ui/types/nodes"; + +// 노드 타입별 패널 매핑 객체 +const PANEL_COMPONENTS: Record> = { + Text: TextPanel, + Heading: TextPanel, + Button: ButtonPanel, + Image: ImagePanel, + Container: LayoutPanel, + Stack: LayoutPanel, + Modal: LayoutPanel, + Group: LayoutPanel, +}; + +export default function RightSidebar() { + const selectedNodeId = useSelectedNodeId(); + const nodes = useCurNodes(); + + const selectedNode = nodes?.find((node) => node.id === selectedNodeId); + + // 노드 타입별 패널 렌더링 로직 (객체 매핑 사용) + const renderPanel = () => { + if (!selectedNode) return null; + + const Panel = PANEL_COMPONENTS[selectedNode.type] || LayoutPanel; + return ; + }; + + return ( +
+ + + {selectedNode ? ( +
+ {renderPanel()} +
+ ) : ( +
+ Select a node to edit its properties +
+ )} +
+ ); +} diff --git a/apps/editor/src/widgets/right-sidebar/ui/SettingsTitle.tsx b/apps/editor/src/widgets/right-sidebar/ui/SettingsTitle.tsx new file mode 100644 index 0000000..aff264a --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/SettingsTitle.tsx @@ -0,0 +1,13 @@ +interface SettingsTitleProps { + type?: string; +} + +export default function SettingsTitle({ type }: SettingsTitleProps) { + const title = type ? `${type.toUpperCase()} SETTINGS` : "NO SELECTION"; + + return ( +

+ {title} +

+ ); +} From cc80fc6996587d5c8793199a8c4e7585d48c333e Mon Sep 17 00:00:00 2001 From: "DESKTOP-4N4R3LH\\jqk17" Date: Mon, 9 Feb 2026 02:06:41 +0900 Subject: [PATCH 07/15] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=97=90=EB=94=94?= =?UTF-8?q?=ED=84=B0=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20RightSidebar?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/editor/src/app/editor/page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/editor/src/app/editor/page.tsx b/apps/editor/src/app/editor/page.tsx index 389f4b4..e48fc65 100644 --- a/apps/editor/src/app/editor/page.tsx +++ b/apps/editor/src/app/editor/page.tsx @@ -2,6 +2,7 @@ import getNodesFromDB from "@/actions/editor/getNodesFromDB"; import Canvas from "@/components/editor/Canvas"; import EditorStoreInitializer from "@/components/editor/EditorStoreInitializer"; import { LeftSidebar } from "@/widgets/left-sidebar"; +import { RightSidebar } from "@/widgets/right-sidebar"; import { RuntimeProvider } from "@repo/ui/context/runtimeContext"; export default async function EditorPage() { @@ -18,6 +19,7 @@ export default async function EditorPage() {
+
From 1f546d922e62656baf6df9c504eaee6c5bc69c9b Mon Sep 17 00:00:00 2001 From: "DESKTOP-4N4R3LH\\jqk17" Date: Mon, 9 Feb 2026 02:07:18 +0900 Subject: [PATCH 08/15] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20Heading?= =?UTF-8?q?=20=EB=B0=8F=20Text=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=EC=9D=98=20=EA=B3=A0=EC=A0=95=20className=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/src/components/Heading.tsx | 2 +- packages/ui/src/components/Text.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/Heading.tsx b/packages/ui/src/components/Heading.tsx index 07ca899..33acd6b 100644 --- a/packages/ui/src/components/Heading.tsx +++ b/packages/ui/src/components/Heading.tsx @@ -15,7 +15,7 @@ export default function HeadingComponent({ {text} diff --git a/packages/ui/src/components/Text.tsx b/packages/ui/src/components/Text.tsx index 8ca9a55..2d829e1 100644 --- a/packages/ui/src/components/Text.tsx +++ b/packages/ui/src/components/Text.tsx @@ -14,7 +14,7 @@ export default function TextComponent({ {text} From ee4f214d167e003914d7d2c2b89634f704600a09 Mon Sep 17 00:00:00 2001 From: "DESKTOP-4N4R3LH\\jqk17" Date: Mon, 9 Feb 2026 02:55:56 +0900 Subject: [PATCH 09/15] =?UTF-8?q?=E2=9C=A8=20feat:=20Modal=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B8=B0=EB=B3=B8=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/shared/lib/component-defaults.ts | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/apps/editor/src/shared/lib/component-defaults.ts b/apps/editor/src/shared/lib/component-defaults.ts index a2bce1f..cb93591 100644 --- a/apps/editor/src/shared/lib/component-defaults.ts +++ b/apps/editor/src/shared/lib/component-defaults.ts @@ -9,6 +9,14 @@ export interface ComponentDefaults { layout: WcxNode['layout']; // x, y, width, height, zIndex } +// 모든 컴포넌트의 기본 레이아웃 스타일 (Flex Row 중앙 정렬) +const DEFAULT_FLEX_STYLE: NodeStyle = { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', +}; + export const COMPONENT_DEFAULTS: Record = { Image: { props: { @@ -16,7 +24,9 @@ export const COMPONENT_DEFAULTS: Record = { alt: "Image", caption: "Image Caption", }, - style: {}, + style: { + ...DEFAULT_FLEX_STYLE, + }, layout: { x: 0, y: 0, @@ -31,9 +41,10 @@ export const COMPONENT_DEFAULTS: Record = { level: "h2", }, style: { + ...DEFAULT_FLEX_STYLE, color: "#000000", fontSize: "24px", - fontWeight: "bold", + fontWeight: "700", }, layout: { x: 0, @@ -49,8 +60,11 @@ export const COMPONENT_DEFAULTS: Record = { level: "h5", }, style: { + ...DEFAULT_FLEX_STYLE, color: "#333333", fontSize: "16px", + background: 'white', + border: '1px solid #ccc', }, layout: { x: 0, @@ -65,12 +79,10 @@ export const COMPONENT_DEFAULTS: Record = { text: "Button", }, style: { + ...DEFAULT_FLEX_STYLE, backgroundColor: "#007bff", color: "#ffffff", borderRadius: "4px", - display: "flex", - alignItems: "center", - justifyContent: "center", }, layout: { x: 0, @@ -85,6 +97,7 @@ export const COMPONENT_DEFAULTS: Record = { tagName: "div", }, style: { + ...DEFAULT_FLEX_STYLE, border: "1px dashed #ccc", backgroundColor: "#ffffff", }, @@ -116,12 +129,10 @@ export const COMPONENT_DEFAULTS: Record = { zIndex: 100, }, }, - // Stack 추가 Stack: { props: {}, style: { - display: "flex", - flexDirection: "column", + ...DEFAULT_FLEX_STYLE, gap: "10px", padding: "20px", backgroundColor: "#f9fafb", @@ -135,10 +146,11 @@ export const COMPONENT_DEFAULTS: Record = { zIndex: 0, }, }, - // Group 추가 Group: { props: {}, - style: {}, + style: { + ...DEFAULT_FLEX_STYLE, + }, layout: { x: 0, y: 0, From 3cb950990eca6face30efa669092048cc278c8ed Mon Sep 17 00:00:00 2001 From: "DESKTOP-4N4R3LH\\jqk17" Date: Fri, 20 Feb 2026 06:52:53 +0900 Subject: [PATCH 10/15] =?UTF-8?q?=E2=9C=A8feat:=20=EC=9A=B0=EC=B8=A1=20?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20=ED=8C=A8=EB=84=90=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1=EC=9A=94=EC=86=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - atoms -> sections -> panels --- .../widgets/right-sidebar/ui/RightSidebar.tsx | 54 ++++--- .../right-sidebar/ui/atoms/AlignPicker.tsx | 11 +- .../ui/atoms/ConstraintPicker.tsx | 82 ++++++++++ .../ui/atoms/DirectionToggle.tsx | 10 +- .../right-sidebar/ui/atoms/FieldRow.tsx | 23 +++ .../right-sidebar/ui/atoms/NumberInput.tsx | 41 +++-- .../right-sidebar/ui/atoms/PaddingInput.tsx | 76 +++++++++ .../right-sidebar/ui/atoms/SelectInput.tsx | 24 ++- .../ui/atoms/SizingModeInput.tsx | 118 ++++++++++++++ .../right-sidebar/ui/atoms/SliderInput.tsx | 42 +++++ .../ui/atoms/ToggleButtonGroup.tsx | 13 +- .../right-sidebar/ui/panels/ButtonPanel.tsx | 4 +- .../right-sidebar/ui/panels/ImagePanel.tsx | 4 +- .../right-sidebar/ui/panels/LayoutPanel.tsx | 108 ------------- .../ui/sections/LayoutSection.tsx | 146 +++++++++++++++++ .../ui/sections/PositionSection.tsx | 100 ++++++++++++ .../ui/sections/PropertySection.tsx | 46 ++++++ .../right-sidebar/ui/sections/SizeSection.tsx | 147 ++++++++++++++++++ 18 files changed, 887 insertions(+), 162 deletions(-) create mode 100644 apps/editor/src/widgets/right-sidebar/ui/atoms/ConstraintPicker.tsx create mode 100644 apps/editor/src/widgets/right-sidebar/ui/atoms/FieldRow.tsx create mode 100644 apps/editor/src/widgets/right-sidebar/ui/atoms/PaddingInput.tsx create mode 100644 apps/editor/src/widgets/right-sidebar/ui/atoms/SizingModeInput.tsx create mode 100644 apps/editor/src/widgets/right-sidebar/ui/atoms/SliderInput.tsx delete mode 100644 apps/editor/src/widgets/right-sidebar/ui/panels/LayoutPanel.tsx create mode 100644 apps/editor/src/widgets/right-sidebar/ui/sections/LayoutSection.tsx create mode 100644 apps/editor/src/widgets/right-sidebar/ui/sections/PositionSection.tsx create mode 100644 apps/editor/src/widgets/right-sidebar/ui/sections/PropertySection.tsx create mode 100644 apps/editor/src/widgets/right-sidebar/ui/sections/SizeSection.tsx diff --git a/apps/editor/src/widgets/right-sidebar/ui/RightSidebar.tsx b/apps/editor/src/widgets/right-sidebar/ui/RightSidebar.tsx index 6a7f590..591fcda 100644 --- a/apps/editor/src/widgets/right-sidebar/ui/RightSidebar.tsx +++ b/apps/editor/src/widgets/right-sidebar/ui/RightSidebar.tsx @@ -5,42 +5,58 @@ import SettingsTitle from "./SettingsTitle"; import TextPanel from "./panels/TextPanel"; import ButtonPanel from "./panels/ButtonPanel"; import ImagePanel from "./panels/ImagePanel"; -import LayoutPanel from "./panels/LayoutPanel"; +import PositionSection from "./sections/PositionSection"; +import SizeSection from "./sections/SizeSection"; +import LayoutSection from "./sections/LayoutSection"; +import PropertySection from "./sections/PropertySection"; import { WcxNode } from "@repo/ui/types/nodes"; -// 노드 타입별 패널 매핑 객체 -const PANEL_COMPONENTS: Record> = { +// 노드 타입별 Content 패널 매핑 +const CONTENT_PANEL: Partial>> = { Text: TextPanel, Heading: TextPanel, Button: ButtonPanel, Image: ImagePanel, - Container: LayoutPanel, - Stack: LayoutPanel, - Modal: LayoutPanel, - Group: LayoutPanel, }; +// Layout 패널을 표시할 노드 타입 +const LAYOUT_TYPES: WcxNode["type"][] = ["Container", "Stack", "Modal", "Group"]; + export default function RightSidebar() { const selectedNodeId = useSelectedNodeId(); const nodes = useCurNodes(); - const selectedNode = nodes?.find((node) => node.id === selectedNodeId); - - // 노드 타입별 패널 렌더링 로직 (객체 매핑 사용) - const renderPanel = () => { - if (!selectedNode) return null; - - const Panel = PANEL_COMPONENTS[selectedNode.type] || LayoutPanel; - return ; - }; + const selectedNode = nodes?.find((n) => n.id === selectedNodeId); + const ContentPanel = selectedNode ? CONTENT_PANEL[selectedNode.type] : undefined; return ( -
+
{selectedNode ? ( -
- {renderPanel()} +
+ + + + + {/* ─── Size ─── */} + + + + + {/* ─── Layout (컨테이너 계열만) ─── */} + {LAYOUT_TYPES.includes(selectedNode.type) && ( + + + + )} + + {/* ─── Content (노드 타입 고유 속성) ─── */} + {ContentPanel && ( + + + + )}
) : (
diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/AlignPicker.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/AlignPicker.tsx index dc9d679..33866fd 100644 --- a/apps/editor/src/widgets/right-sidebar/ui/atoms/AlignPicker.tsx +++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/AlignPicker.tsx @@ -19,9 +19,6 @@ interface AlignPickerProps { } export default function AlignPicker({ direction, value, onChange }: AlignPickerProps) { - // 교차축(Align)의 방향 결정 - // Row(가로)일 때 교차축은 세로 정렬 아이콘이 필요함 - // Column(세로)일 때 교차축은 가로 정렬 아이콘이 필요함 const isDirectionRow = direction === "row"; const options = isDirectionRow @@ -37,7 +34,7 @@ export default function AlignPicker({ direction, value, onChange }: AlignPickerP ]; return ( -
+
{options.map((opt) => { const Icon = opt.icon; const isActive = value === opt.value; @@ -48,14 +45,14 @@ export default function AlignPicker({ direction, value, onChange }: AlignPickerP type="button" onClick={() => onChange(opt.value)} className={cn( - "flex h-7 w-9 items-center justify-center rounded-[6px] transition-all", + "flex-1 flex h-full items-center justify-center rounded-[6px] transition-all", isActive - ? "bg-white shadow-sm text-[#8B5CF6]" // 이미지의 보라색 포인트 반영 + ? "bg-white shadow-sm text-[#8B5CF6]" : "text-zinc-400 hover:text-zinc-600" )} title={opt.label} > - + ); })} diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/ConstraintPicker.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/ConstraintPicker.tsx new file mode 100644 index 0000000..b2e4647 --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/ConstraintPicker.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { cn } from "@repo/utils"; + +interface ConstraintPickerProps { + top: boolean; + right: boolean; + bottom: boolean; + left: boolean; + onChange: (constraints: { top: boolean; right: boolean; bottom: boolean; left: boolean }) => void; +} + +/** + * Framer의 시각적 Position Constraint Picker + * 중앙 점 + 상/하/좌/우 연결선을 클릭하여 핀 활성화/비활성화 + */ +export default function ConstraintPicker({ + top, + right, + bottom, + left, + onChange, +}: ConstraintPickerProps) { + const toggle = (key: "top" | "right" | "bottom" | "left") => { + onChange({ top, right, bottom, left, [key]: !{ top, right, bottom, left }[key] }); + }; + + return ( +
+ {/* 중앙 점 */} +
+ + {/* Top 연결선 */} + + + {/* Bottom 연결선 */} + + + {/* Left 연결선 */} + + + {/* Right 연결선 */} + +
+ ); +} diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/DirectionToggle.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/DirectionToggle.tsx index 7b0b5cc..4adfb22 100644 --- a/apps/editor/src/widgets/right-sidebar/ui/atoms/DirectionToggle.tsx +++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/DirectionToggle.tsx @@ -10,32 +10,32 @@ interface DirectionToggleProps { export default function DirectionToggle({ value, onChange }: DirectionToggleProps) { return ( -
+
); diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/FieldRow.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/FieldRow.tsx new file mode 100644 index 0000000..fe497ce --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/FieldRow.tsx @@ -0,0 +1,23 @@ +"use client"; + +interface FieldRowProps { + label: string; + children: React.ReactNode; +} + +/** + * 사이드바 필드 행 — 가로 한 줄 + * [라벨] [컨트롤] + */ +export default function FieldRow({ label, children }: FieldRowProps) { + return ( +
+ + {label} + +
+ {children} +
+
+ ); +} diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/NumberInput.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/NumberInput.tsx index 475da6d..9d3a284 100644 --- a/apps/editor/src/widgets/right-sidebar/ui/atoms/NumberInput.tsx +++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/NumberInput.tsx @@ -5,17 +5,40 @@ interface NumberInputProps { name?: string; value: number; onChange: (val: number) => void; + size?: "single" | "small"; + suffix?: string; + disabled?: boolean; + blurred?: boolean; } -export default function NumberInput({ id, name, value, onChange }: NumberInputProps) { +export default function NumberInput({ + id, + name, + value, + onChange, + size = "single", + suffix, + disabled, + blurred, +}: NumberInputProps) { + const widthClass = size === "small" ? "w-[65px]" : "w-[140px]"; + return ( - onChange(Number(e.target.value))} - className="flex w-full h-[33px] items-center px-3 py-2 rounded-[6px] border border-[#E4E4E7] bg-white font-inter font-normal text-[14px] leading-none text-zinc-900 outline-none focus:border-zinc-400 transition-colors [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - /> +
+ onChange(Number(e.target.value))} + disabled={disabled || blurred} + className={`flex w-full h-[28px] items-center pl-3 ${suffix ? "pr-8" : "pr-3"} py-0 rounded-[8px] border-transparent bg-[#F4F4F5] font-inter font-normal text-[13px] leading-none text-zinc-900 outline-none focus:border-zinc-400 transition-colors [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${blurred ? "pointer-events-none" : ""}`} + /> + {suffix && ( + + {suffix} + + )} +
); } diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/PaddingInput.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/PaddingInput.tsx new file mode 100644 index 0000000..e14cdb3 --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/PaddingInput.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { useState } from "react"; +import NumberInput from "./NumberInput"; +import ToggleButtonGroup from "./ToggleButtonGroup"; + +type PaddingMode = "uniform" | "individual"; + +interface PaddingInputProps { + top: number; + right: number; + bottom: number; + left: number; + onChange: (padding: { top: number; right: number; bottom: number; left: number }) => void; +} + +export default function PaddingInput({ + top, + right, + bottom, + left, + onChange, +}: PaddingInputProps) { + const [mode, setMode] = useState( + top === right && right === bottom && bottom === left ? "uniform" : "individual" + ); + + const handleUniformChange = (value: number) => { + onChange({ top: value, right: value, bottom: value, left: value }); + }; + + const handleIndividualChange = (key: "top" | "right" | "bottom" | "left", value: number) => { + onChange({ top, right, bottom, left, [key]: value }); + }; + + return ( +
+
+ + setMode(val as PaddingMode)} + options={[ + { label: "U", value: "uniform" }, + { label: "I", value: "individual" }, + ]} + /> +
+ + {mode === "individual" && ( +
+
+ {(["top", "right", "bottom", "left"] as const).map((key) => ( +
+ handleIndividualChange(key, Number(e.target.value))} + className="w-full h-[26px] text-center bg-[#F4F4F5] rounded-[4px] text-[10px] font-medium outline-none border-none focus:ring-1 focus:ring-blue-500 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + /> + {key[0]} +
+ ))} +
+
+ )} +
+ ); +} diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/SelectInput.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/SelectInput.tsx index 16327f2..fcf3622 100644 --- a/apps/editor/src/widgets/right-sidebar/ui/atoms/SelectInput.tsx +++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/SelectInput.tsx @@ -6,17 +6,27 @@ interface SelectInputProps { value: string; options: { label: string; value: string }[]; onChange: (val: string) => void; + size?: "single" | "small"; } -export default function SelectInput({ id, name, value, options, onChange }: SelectInputProps) { +export default function SelectInput({ + id, + name, + value, + options, + onChange, + size = "single", +}: SelectInputProps) { + const widthClass = size === "small" ? "w-[65px]" : "w-[140px]"; + return ( -
+
- {/* 커스텀 화살표 아이콘 (간단한 구현) */} -
- + {/* 커스텀 화살표 아이콘 */} +
+
); } + + diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/SizingModeInput.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/SizingModeInput.tsx new file mode 100644 index 0000000..87a28c0 --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/SizingModeInput.tsx @@ -0,0 +1,118 @@ +"use client"; + +import { cn } from "@repo/utils"; +import { ChevronDown } from "lucide-react"; +import { useState, useRef, useEffect } from "react"; + +type SizingMode = "fixed" | "fill" | "fit" | "relative"; +type Unit = "px" | "%"; + +interface SizingModeInputProps { + mode: SizingMode; + value: number | string; + unit: Unit; + label: string; // "W" 또는 "H" + onModeChange: (mode: SizingMode) => void; + onValueChange: (value: number) => void; + onUnitChange: (unit: Unit) => void; +} + +const MODE_LABELS: Record = { + fixed: "Fixed", + fill: "Fill", + fit: "Fit", + relative: "Relative", +}; + +/** + * Framer의 Size 입력 컴포넌트 + * 모드 선택 + 값 입력 + 단위 토글 + */ +export default function SizingModeInput({ + mode, + value, + unit, + label, + onModeChange, + onValueChange, + onUnitChange, +}: SizingModeInputProps) { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + // 모드에 따라 입력 비활성화 + const isDisabled = mode === "fill" || mode === "fit"; + const displayValue = isDisabled ? (mode === "fill" ? "1fr" : "auto") : value; + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { + setIsOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + return ( +
+ {/* 라벨 */} + {label} + + {/* 모드 드롭다운 */} +
+ + {isOpen && ( +
+ {(Object.keys(MODE_LABELS) as SizingMode[]).map((m) => ( + + ))} +
+ )} +
+ + {/* 값 입력 */} + onValueChange(Number(e.target.value))} + className={cn( + "w-16 px-2 py-1.5 text-xs border border-zinc-200 rounded-md text-center", + isDisabled && "bg-zinc-100 text-zinc-400 cursor-not-allowed" + )} + /> + + {/* 단위 토글 */} + {!isDisabled && ( + + )} +
+ ); +} diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/SliderInput.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/SliderInput.tsx new file mode 100644 index 0000000..f728f94 --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/SliderInput.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { cn } from "@repo/utils"; + +interface SliderInputProps { + value: number; + min?: number; + max?: number; + onChange: (value: number) => void; + size?: "single" | "small"; +} + +export default function SliderInput({ + value, + min = 0, + max = 100, + onChange, + size = "small", +}: SliderInputProps) { + const widthClass = size === "small" ? "w-[65px]" : "w-[140px]"; + + return ( +
+ onChange(Number(e.target.value))} + className={cn( + "w-full h-1 bg-zinc-200 rounded-full appearance-none cursor-pointer accent-[#2563EB]", + "[&::-webkit-slider-thumb]:appearance-none", + "[&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5", + "[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white", + "[&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-[#2563EB]", + "[&::-webkit-slider-thumb]:shadow-md [&::-webkit-slider-thumb]:transition-transform", + "hover:[&::-webkit-slider-thumb]:scale-110" + )} + /> +
+ ); +} diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/ToggleButtonGroup.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/ToggleButtonGroup.tsx index 01472d6..786cf18 100644 --- a/apps/editor/src/widgets/right-sidebar/ui/atoms/ToggleButtonGroup.tsx +++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/ToggleButtonGroup.tsx @@ -11,15 +11,19 @@ interface ToggleButtonGroupProps { options: ToggleOption[]; value: string | string[]; onChange: (val: string) => void; + size?: "single" | "small"; } export default function ToggleButtonGroup({ options, value, onChange, + size = "single", }: ToggleButtonGroupProps) { + const widthClass = size === "small" ? "w-[65px]" : "w-[140px]"; + return ( -
+
{options.map((opt) => { const isActive = Array.isArray(value) ? value.includes(opt.value) @@ -28,12 +32,13 @@ export default function ToggleButtonGroup({ return ( + + {/* 본문 */} + {isOpen && ( +
+ {children} +
+ )} +
+ ); +} diff --git a/apps/editor/src/widgets/right-sidebar/ui/sections/SizeSection.tsx b/apps/editor/src/widgets/right-sidebar/ui/sections/SizeSection.tsx new file mode 100644 index 0000000..3b33178 --- /dev/null +++ b/apps/editor/src/widgets/right-sidebar/ui/sections/SizeSection.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { WcxNode } from "@repo/ui/types/nodes"; +import { useUpdateNode, useUpdateNodeLayout, useCurNodes } from "@/stores/useEditorStore"; +import FieldRow from "../atoms/FieldRow"; +import NumberInput from "../atoms/NumberInput"; +import SelectInput from "../atoms/SelectInput"; + +interface SizeSectionProps { + node: WcxNode; +} + +type SizingMode = "fixed" | "fill" | "fit" | "relative"; + +export default function SizeSection({ node }: SizeSectionProps) { + const updateLayout = useUpdateNodeLayout(); + const updateNode = useUpdateNode(); + const nodes = useCurNodes(); + + const widthMode: SizingMode = (node.layout as any).widthMode || "fixed"; + const heightMode: SizingMode = (node.layout as any).heightMode || "fixed"; + + const widthValue = typeof node.layout.width === "number" + ? node.layout.width + : parseInt(String(node.layout.width)) || 0; + + const heightValue = typeof node.layout.height === "number" + ? node.layout.height + : parseInt(String(node.layout.height)) || 0; + + const handleStyleChange = (key: string, value: any) => { + updateNode(node.id, { style: { ...node.style, [key]: value } }); + }; + + const convertValue = (oldValue: number, oldMode: SizingMode, newMode: SizingMode, isHeight: boolean) => { + if (oldMode === newMode) return oldValue; + if (newMode === 'fill') return 1; + if (newMode === 'fit') return oldValue; + + // Fixed -> Relative + if (oldMode === 'fixed' && newMode === 'relative') { + const parent = nodes?.find(n => n.id === node.parent_id); + const parentSize = isHeight + ? (typeof parent?.layout.height === 'number' ? parent.layout.height : 1000) + : (typeof parent?.layout.width === 'number' ? parent.layout.width : 1000); + return Math.round((oldValue / parentSize) * 100); + } + + // Relative -> Fixed + if (oldMode === 'relative' && newMode === 'fixed') { + const parent = nodes?.find(n => n.id === node.parent_id); + const parentSize = isHeight + ? (typeof parent?.layout.height === 'number' ? parent.layout.height : 1000) + : (typeof parent?.layout.width === 'number' ? parent.layout.width : 1000); + return Math.round((oldValue / 100) * parentSize); + } + + return oldValue; + }; + + const handleModeChange = (key: 'widthMode' | 'heightMode', newMode: SizingMode) => { + const isHeight = key === 'heightMode'; + const sizeKey = isHeight ? 'height' : 'width'; + const oldMode = isHeight ? heightMode : widthMode; + const oldValue = isHeight ? heightValue : widthValue; + + const newValue = convertValue(oldValue, oldMode, newMode, isHeight); + updateLayout(node.id, { [key]: newMode, [sizeKey]: newValue } as any); + }; + + const modeOptions = [ + { label: "Fixed", value: "fixed" }, + { label: "Rel", value: "relative" }, + { label: "Fill", value: "fill" }, + { label: "Fit", value: "fit" }, + ]; + + return ( + <> + {/* Width: 값 / 모드 */} + + updateLayout(node.id, { width: v })} + size="small" + suffix={widthMode === "relative" ? "%" : widthMode === "fill" ? "fr" : undefined} + blurred={widthMode === "fit"} + /> + handleModeChange("widthMode", v as SizingMode)} + size="small" + /> + + + {/* Height: 값 / 모드 */} + + updateLayout(node.id, { height: v })} + size="small" + suffix={heightMode === "relative" ? "%" : heightMode === "fill" ? "fr" : undefined} + blurred={heightMode === "fit"} + /> + handleModeChange("heightMode", v as SizingMode)} + size="small" + /> + + + {/* Min Width */} + + handleStyleChange("minWidth", v)} + /> + + + {/* Max Width */} + + handleStyleChange("maxWidth", v)} + /> + + + {/* Min Height */} + + handleStyleChange("minHeight", v)} + /> + + + {/* Max Height */} + + handleStyleChange("maxHeight", v)} + /> + + + ); +} From a827ce6909b0fc6ae1c10786d8c53f43a8e3e980 Mon Sep 17 00:00:00 2001 From: "DESKTOP-4N4R3LH\\jqk17" Date: Fri, 20 Feb 2026 12:23:56 +0900 Subject: [PATCH 11/15] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=99=BC=EC=AA=BD=20?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=EC=97=90=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=20=EB=B6=84=EC=84=9D=20=ED=8C=A8=EB=84=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parent_id 기반의 노드 계층 트리 구조 시각화 구현 레이어 클릭 시 에디터 노드 선택 연동 노드 타입별 아이콘 및 요약 정보 표시 로직 추가 --- .../widgets/left-sidebar/model/constants.ts | 6 +- .../src/widgets/left-sidebar/model/types.ts | 2 +- .../src/widgets/left-sidebar/ui/SubPanel.tsx | 24 +-- .../left-sidebar/ui/sub-panels/LayerPanel.tsx | 183 ++++++++++++++++++ .../left-sidebar/ui/sub-panels/index.ts | 3 +- 5 files changed, 203 insertions(+), 15 deletions(-) create mode 100644 apps/editor/src/widgets/left-sidebar/ui/sub-panels/LayerPanel.tsx diff --git a/apps/editor/src/widgets/left-sidebar/model/constants.ts b/apps/editor/src/widgets/left-sidebar/model/constants.ts index 8bd93d4..23f83c0 100644 --- a/apps/editor/src/widgets/left-sidebar/model/constants.ts +++ b/apps/editor/src/widgets/left-sidebar/model/constants.ts @@ -3,7 +3,8 @@ import { File, PanelsTopLeft, PlusCircle, - AppWindow + AppWindow, + Plus } from 'lucide-react'; export const STATIC_PANEL_DATA = { @@ -32,7 +33,8 @@ export const STATIC_PANEL_DATA = { }; export const NAV_ITEMS = [ - { id: 'component', label: '컴포넌트', icon: Layers }, + { id: 'layer', label: '레이어', icon: Layers }, + { id: 'component', label: '컴포넌트', icon: Plus }, { id: 'page', label: '페이지', icon: File }, { id: 'section', label: '섹션', icon: PanelsTopLeft }, { id: 'widget', label: '위젯', icon: PlusCircle }, diff --git a/apps/editor/src/widgets/left-sidebar/model/types.ts b/apps/editor/src/widgets/left-sidebar/model/types.ts index 3ed797d..5b634d8 100644 --- a/apps/editor/src/widgets/left-sidebar/model/types.ts +++ b/apps/editor/src/widgets/left-sidebar/model/types.ts @@ -1,4 +1,4 @@ -export type NavTabId = 'component' | 'page' | 'section' | 'widget' | 'modal'; +export type NavTabId = 'layer' | 'component' | 'page' | 'section' | 'widget' | 'modal'; export interface NavigationState { activeTab: NavTabId | null; diff --git a/apps/editor/src/widgets/left-sidebar/ui/SubPanel.tsx b/apps/editor/src/widgets/left-sidebar/ui/SubPanel.tsx index da0521a..03b4e80 100644 --- a/apps/editor/src/widgets/left-sidebar/ui/SubPanel.tsx +++ b/apps/editor/src/widgets/left-sidebar/ui/SubPanel.tsx @@ -1,10 +1,11 @@ import { useNavigationStore } from '../model/store'; -import { - ComponentPanel, - PagePanel, - SectionPanel, - WidgetPanel, - ModalPanel +import { + ComponentPanel, + LayerPanel, + PagePanel, + SectionPanel, + WidgetPanel, + ModalPanel } from './sub-panels'; export const SubPanel = () => { @@ -15,11 +16,12 @@ export const SubPanel = () => { const renderContent = () => { switch (activeTab) { case 'component': return ; - case 'page': return ; - case 'section': return ; - case 'widget': return ; - case 'modal': return ; - default: return null; + case 'layer': return ; + case 'page': return ; + case 'section': return ; + case 'widget': return ; + case 'modal': return ; + default: return null; } }; diff --git a/apps/editor/src/widgets/left-sidebar/ui/sub-panels/LayerPanel.tsx b/apps/editor/src/widgets/left-sidebar/ui/sub-panels/LayerPanel.tsx new file mode 100644 index 0000000..6c1b262 --- /dev/null +++ b/apps/editor/src/widgets/left-sidebar/ui/sub-panels/LayerPanel.tsx @@ -0,0 +1,183 @@ +import { PanelBaseLayout } from './base/PanelBaseLayout'; +import { useCurNodes, useSelectNode, useSelectedNodeId } from '../../../../stores/useEditorStore'; +import { cn } from '@repo/utils'; +import { + Type, + Image as ImageIcon, + Heading1, + Square, + Box, + Layout, + Component, + ChevronRight, + ChevronDown, + Layers +} from 'lucide-react'; +import { useState } from 'react'; +import { WcxNode } from '@repo/ui/types/nodes'; + +/** + * 노드 타입별 아이콘 매핑 + */ +const NODE_TYPE_ICONS: Record = { + Text: Type, + Image: ImageIcon, + Heading: Heading1, + Button: Square, + Container: Box, + Stack: Layout, + Group: Component, + Modal: Layout, +}; + +interface LayerItemProps { + node: WcxNode; // 현재 렌더링할 노드 + nodes: WcxNode[]; // 전체 노드 배열 (자식 탐색용) + selectedId: string | null; // 현재 선택된 노드 ID + onSelect: (id: string) => void; // 노드 선택 핸들러 + depth: number; // 계층 깊이 (들여쓰기 계산용) +} + +/** + * 개별 레이어 아이템 컴포넌트 (재귀적으로 자식 노드 렌더링) + */ +const LayerItem = ({ node, nodes, selectedId, onSelect, depth }: LayerItemProps) => { + // 폴더 접기/펴기 상태 + const [isExpanded, setIsExpanded] = useState(true); + + // 현재 노드를 부모로 가지는 자식 노드들을 필터링하고 position 순으로 정렬 + const children = nodes + .filter((n) => n.parent_id === node.id) + .sort((a, b) => a.position - b.position); + + const hasChildren = children.length > 0; + const isSelected = selectedId === node.id; + const Icon = NODE_TYPE_ICONS[node.type] || Box; + + /** + * 레이어 리스트에 표시될 노드의 이름 결정 + * 1. 텍스트 컴포넌트면 해당 텍스트 내용 우선 + * 2. 이미지면 alt 텍스트 우선 + * 3. 위 조건에 해당 없으면 노드 타입 표시 + */ + const getNodeName = () => { + if ('props' in node) { + const props = node.props as any; + if (props.text) return props.text; + if (props.alt) return props.alt; + } + return node.type; + }; + + return ( +
+ {/* 레이어 행 (클릭 시 선택) */} +
onSelect(node.id)} + > + {/* 접기/펴기 버튼 (자식이 있을 때만 노출) */} +
+ {hasChildren && ( + + )} +
+ + {/* 노드 타입 아이콘 */} + + + {/* 노드 이름 */} + + {getNodeName()} + +
+ + {/* 자식 노드 재귀 호출 */} + {hasChildren && isExpanded && ( +
+ {children.map((child) => ( + + ))} +
+ )} +
+ ); +}; + +/** + * 레이어 패널 메인 컴포넌트 + */ +export const LayerPanel = () => { + // 스토어에서 전체 노드와 선택 정보 가져오기 + const nodes = useCurNodes() || []; + const selectedId = useSelectedNodeId(); + const selectNode = useSelectNode(); + + // 최상위 노드(부모가 없는 노드)들만 먼저 추출 + const rootNodes = nodes + .filter((node) => node.parent_id === null) + .sort((a, b) => a.position - b.position); + + return ( + +
+ {rootNodes.length > 0 ? ( + // 최상위 노드부터 렌더링 시작 (이후 내부에서 자식들을 재귀적으로 그림) + rootNodes.map((node) => ( + + )) + ) : ( + // 노드가 없을 때의 빈 화면 +
+ +

레이어가 없습니다.
컴포넌트를 추가해보세요.

+
+ )} +
+
+ ); +}; diff --git a/apps/editor/src/widgets/left-sidebar/ui/sub-panels/index.ts b/apps/editor/src/widgets/left-sidebar/ui/sub-panels/index.ts index e3d03fb..ebd9d5e 100644 --- a/apps/editor/src/widgets/left-sidebar/ui/sub-panels/index.ts +++ b/apps/editor/src/widgets/left-sidebar/ui/sub-panels/index.ts @@ -2,4 +2,5 @@ export * from './ComponentPanel'; export * from './PagePanel'; export * from './SectionPanel'; export * from './WidgetPanel'; -export * from './ModalPanel'; \ No newline at end of file +export * from './ModalPanel'; +export * from './LayerPanel'; From f2eca7cd27b3e0530efba7400b06a4c6b7f90b3a Mon Sep 17 00:00:00 2001 From: "DESKTOP-4N4R3LH\\jqk17" Date: Fri, 20 Feb 2026 16:55:36 +0900 Subject: [PATCH 12/15] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EB=85=B8=EB=93=9C=20=EC=9C=84=EC=B9=98=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=AA=A8=EB=8D=B8=EC=9D=84=20x,=20y=20=EC=A2=8C?= =?UTF-8?q?=ED=91=9C=EB=A1=9C=20=EB=8B=A8=EC=9D=BC=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/sections/PositionSection.tsx | 37 ++++++++----------- packages/ui/src/core/EditorNodeWrapper.tsx | 7 +++- packages/ui/src/types/nodes.ts | 8 +++- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/apps/editor/src/widgets/right-sidebar/ui/sections/PositionSection.tsx b/apps/editor/src/widgets/right-sidebar/ui/sections/PositionSection.tsx index 5399678..030d1b1 100644 --- a/apps/editor/src/widgets/right-sidebar/ui/sections/PositionSection.tsx +++ b/apps/editor/src/widgets/right-sidebar/ui/sections/PositionSection.tsx @@ -1,7 +1,7 @@ "use client"; import { WcxNode } from "@repo/ui/types/nodes"; -import { useUpdateNode } from "@/stores/useEditorStore"; +import { useUpdateNode, useUpdateNodeLayout } from "@/stores/useEditorStore"; import FieldRow from "../atoms/FieldRow"; import NumberInput from "../atoms/NumberInput"; import SelectInput from "../atoms/SelectInput"; @@ -14,14 +14,20 @@ type PositionType = "relative" | "absolute" | "fixed" | "sticky"; export default function PositionSection({ node }: PositionSectionProps) { const updateNode = useUpdateNode(); + const updateNodeLayout = useUpdateNodeLayout(); const positionType = (node.style.position as PositionType) || "relative"; const handleStyleChange = (key: string, value: any) => { updateNode(node.id, { style: { ...node.style, [key]: value } }); }; + const handleLayoutChange = (key: string, value: any) => { + updateNodeLayout(node.id, { [key]: value }); + }; + const handlePositionTypeChange = (type: PositionType) => { if (type === "relative") { + // relative 전환 시 스타일에서 위치 정보 제거 updateNode(node.id, { style: { ...node.style, @@ -32,6 +38,7 @@ export default function PositionSection({ node }: PositionSectionProps) { left: undefined, }, }); + // x, y 좌표는 유지하거나 필요에 따라 초기화할 수 있지만 일단 유지 } else { handleStyleChange("position", type); } @@ -42,42 +49,30 @@ export default function PositionSection({ node }: PositionSectionProps) { return ( <> - {/* Absolute / Fixed → T R B L */} + {/* Absolute / Fixed → Top(y) / Left(x) (Layout 필드 사용) */} {showOffsets && ( <> handleStyleChange("top", v)} - /> - - - handleStyleChange("right", v)} - /> - - - handleStyleChange("bottom", v)} + value={node.layout.y ?? 0} + onChange={(v) => handleLayoutChange("y", v)} /> handleStyleChange("left", v)} + value={node.layout.x ?? 0} + onChange={(v) => handleLayoutChange("x", v)} /> )} - {/* Sticky → Top only */} + {/* Sticky → Top only (y 사용) */} {showStickyTop && ( handleStyleChange("top", v)} + value={node.layout.y ?? 0} + onChange={(v) => handleLayoutChange("y", v)} /> )} diff --git a/packages/ui/src/core/EditorNodeWrapper.tsx b/packages/ui/src/core/EditorNodeWrapper.tsx index ac27061..e197804 100644 --- a/packages/ui/src/core/EditorNodeWrapper.tsx +++ b/packages/ui/src/core/EditorNodeWrapper.tsx @@ -38,7 +38,7 @@ export default function EditorNodeWrapper({ }; const { id } = node; - const { width, height, x, y } = node.layout; + const { width, height, x, y, zIndex } = node.layout; const selectedNodeGuideClasses = { handle: "bg-white border-2 rounded-full border-rnd-handle !w-2 !h-2 ", outline: "ring ring-2 ring-rnd-handle", @@ -50,6 +50,11 @@ export default function EditorNodeWrapper({ e.stopPropagation()} //TODO-일단 이동중에 스토어 업데이트는 미루기 -> 성능 이슈 diff --git a/packages/ui/src/types/nodes.ts b/packages/ui/src/types/nodes.ts index 04d977f..0aa0d11 100644 --- a/packages/ui/src/types/nodes.ts +++ b/packages/ui/src/types/nodes.ts @@ -19,8 +19,12 @@ export interface BaseNode { layout: { x: number; y: number; - width: number; // 혹은 string ('100%') - height: number; // 혹은 string ('auto') + width: number | string; // 숫자(px) 또는 문자열('100%', 'auto') + height: number | string; // 숫자(px) 또는 문자열('100%', 'auto') + widthMode: 'fixed' | 'fill' | 'fit' | 'relative'; // Framer sizing 모드 + heightMode: 'fixed' | 'fill' | 'fit' | 'relative'; // Framer sizing 모드 + widthUnit: 'px' | '%'; + heightUnit: 'px' | '%'; zIndex: number; }; } From e444e57420aa5c6f8d07dcd9a21d253e9c4ecc54 Mon Sep 17 00:00:00 2001 From: "DESKTOP-4N4R3LH\\jqk17" Date: Fri, 20 Feb 2026 17:12:04 +0900 Subject: [PATCH 13/15] =?UTF-8?q?=F0=9F=90=9Bfix:=20CI=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/editor/src/app/editor/page.tsx | 202 +++++++++++++++++- .../src/entities/section/model/store.ts | 2 +- apps/editor/src/stores/useEditorStore.ts | 2 +- .../left-sidebar/ui/sub-panels/LayerPanel.tsx | 11 +- .../widgets/right-sidebar/ui/RightSidebar.tsx | 1 + .../right-sidebar/ui/panels/ButtonPanel.tsx | 2 +- .../right-sidebar/ui/panels/ImagePanel.tsx | 4 +- .../right-sidebar/ui/panels/TextPanel.tsx | 4 +- .../ui/sections/LayoutSection.tsx | 2 +- .../ui/sections/PositionSection.tsx | 4 +- .../ui/sections/PropertySection.tsx | 2 +- .../right-sidebar/ui/sections/SizeSection.tsx | 7 +- 12 files changed, 218 insertions(+), 25 deletions(-) diff --git a/apps/editor/src/app/editor/page.tsx b/apps/editor/src/app/editor/page.tsx index e48fc65..030c7db 100644 --- a/apps/editor/src/app/editor/page.tsx +++ b/apps/editor/src/app/editor/page.tsx @@ -7,16 +7,206 @@ import { RuntimeProvider } from "@repo/ui/context/runtimeContext"; export default async function EditorPage() { const pageId = 201; //목데이터입니다. - //서버 액션 함수(nodes데이터 패칭함수) - const nodes = await getNodesFromDB(pageId); + // 중첩 구조 실험을 위한 테스트 데이터 + // 레이어 기능을 테스트하기 위한 복잡한 중첩 구조 데이터 + const nodes: any[] = [ + { + id: "root-container", + type: "Stack", + page_id: pageId, + parent_id: null, + position: 1, + style: { + display: "flex", + flexDirection: "column", + backgroundColor: "#ffffff", + padding: "0px", + gap: "0px", + borderRadius: "0px", + position: "absolute", + overflow: "hidden", + border: "1px solid #e5e7eb", + boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1)", + }, + layout: { + x: 50, y: 50, width: 800, height: 600, zIndex: 1, + widthMode: "fixed", heightMode: "fixed", + widthUnit: "px", heightUnit: "px" + } + }, + // --- Navigation Bar --- + { + id: "navbar", + type: "Stack", + page_id: pageId, + parent_id: "root-container", + position: 1, + style: { + display: "flex", + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + padding: "0 24px", + height: "64px", + backgroundColor: "#ffffff", + borderBottom: "1px solid #f3f4f6", + }, + layout: { + x: 0, y: 0, width: "100%", height: 64, zIndex: 1, + widthMode: "fill", heightMode: "fixed", + widthUnit: "%", heightUnit: "px" + } + }, + { + id: "logo", + type: "Text", + page_id: pageId, + parent_id: "navbar", + position: 1, + props: { text: "WCX Editor", level: "h4" }, + style: { fontWeight: "900", color: "#111827", fontSize: "18px" }, + layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } + }, + { + id: "nav-links", + type: "Stack", + page_id: pageId, + parent_id: "navbar", + position: 2, + style: { display: "flex", flexDirection: "row", gap: "20px", alignItems: "center" }, + layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } + }, + { + id: "link-home", + type: "Text", + page_id: pageId, + parent_id: "nav-links", + position: 1, + props: { text: "Home", level: "h6" }, + style: { color: "#4b5563", fontSize: "14px" }, + layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } + }, + { + id: "link-about", + type: "Text", + page_id: pageId, + parent_id: "nav-links", + position: 2, + props: { text: "About", level: "h6" }, + style: { color: "#4b5563", fontSize: "14px" }, + layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } + }, + // --- Hero Section --- + { + id: "hero-section", + type: "Stack", + page_id: pageId, + parent_id: "root-container", + position: 2, + style: { + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + flex: 1, + padding: "60px 20px", + backgroundColor: "#f9fafb", + textAlign: "center" + }, + layout: { + x: 0, y: 0, width: "100%", height: "auto", zIndex: 1, + widthMode: "fill", heightMode: "fill", + widthUnit: "%", heightUnit: "px" + } + }, + { + id: "hero-title", + type: "Heading", + page_id: pageId, + parent_id: "hero-section", + position: 1, + props: { text: "Build Your Web App Faster", level: "h1" }, + style: { color: "#111827", fontWeight: "800", marginBottom: "16px", fontSize: "36px" }, + layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } + }, + { + id: "hero-desc", + type: "Text", + page_id: pageId, + parent_id: "hero-section", + position: 2, + props: { text: "The most powerful no-code editor for modern web developers.", level: "h5" }, + style: { color: "#6b7280", maxWidth: "500px", marginBottom: "32px" }, + layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } + }, + { + id: "hero-cta-container", + type: "Stack", + page_id: pageId, + parent_id: "hero-section", + position: 3, + style: { display: "flex", flexDirection: "row", gap: "12px" }, + layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } + }, + { + id: "btn-primary", + type: "Button", + page_id: pageId, + parent_id: "hero-cta-container", + position: 1, + props: { text: "Get Started" }, + style: { backgroundColor: "#000000", color: "#ffffff", padding: "12px 24px", borderRadius: "8px", fontWeight: "600" }, + layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } + }, + { + id: "btn-secondary", + type: "Button", + page_id: pageId, + parent_id: "hero-cta-container", + position: 2, + props: { text: "View Demo" }, + style: { backgroundColor: "#ffffff", color: "#000000", padding: "12px 24px", borderRadius: "8px", border: "1px solid #e5e7eb", fontWeight: "600" }, + layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } + }, + // --- Footer --- + { + id: "footer", + type: "Stack", + page_id: pageId, + parent_id: "root-container", + position: 3, + style: { + display: "flex", + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + padding: "20px", + backgroundColor: "#ffffff", + borderTop: "1px solid #f3f4f6" + }, + layout: { + x: 0, y: 0, width: "100%", height: 60, zIndex: 1, + widthMode: "fill", heightMode: "fixed", + widthUnit: "%", heightUnit: "px" + } + }, + { + id: "footer-text", + type: "Text", + page_id: pageId, + parent_id: "footer", + position: 1, + props: { text: "© 2024 WebCreatorX. All rights reserved.", level: "h6" }, + style: { color: "#9ca3af", fontSize: "12px" }, + layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } + } + ]; return ( -
-

에디터 페이지 입니다.

+
- {/* 다른 에디터 관련 컴포넌트들은 이곳에서 렌더링 됩니다!(사이드바,매니패스트 수정 컴포넌트 등등...) */} -
+
diff --git a/apps/editor/src/entities/section/model/store.ts b/apps/editor/src/entities/section/model/store.ts index b2045cb..cd03dc9 100644 --- a/apps/editor/src/entities/section/model/store.ts +++ b/apps/editor/src/entities/section/model/store.ts @@ -10,7 +10,7 @@ interface SectionState { selectSection: (id: string) => void; } -export const useSectionStore = create((set) => ({ +export const useSectionStore = create(() => ({ sections: [ { id: 's1', name: '히어로 섹션' }, { id: 's2', name: '특징 소개' }, diff --git a/apps/editor/src/stores/useEditorStore.ts b/apps/editor/src/stores/useEditorStore.ts index 848d58c..2d6f21e 100644 --- a/apps/editor/src/stores/useEditorStore.ts +++ b/apps/editor/src/stores/useEditorStore.ts @@ -128,7 +128,7 @@ const useEditorStore = create( // 1. 최상위 속성 업데이트 (type 등) // (주의: 객체 타입인 style, props, layout을 통째로 덮어쓰지 않도록 별도 처리) - const { style, props, layout, ...rest } = updates; + const { style, props, layout: _layout, ...rest } = updates; Object.assign(targetNode, rest); // 2. 하위 객체 병합 업데이트 diff --git a/apps/editor/src/widgets/left-sidebar/ui/sub-panels/LayerPanel.tsx b/apps/editor/src/widgets/left-sidebar/ui/sub-panels/LayerPanel.tsx index 6c1b262..7aa978a 100644 --- a/apps/editor/src/widgets/left-sidebar/ui/sub-panels/LayerPanel.tsx +++ b/apps/editor/src/widgets/left-sidebar/ui/sub-panels/LayerPanel.tsx @@ -11,7 +11,8 @@ import { Component, ChevronRight, ChevronDown, - Layers + Layers, + LucideIcon } from 'lucide-react'; import { useState } from 'react'; import { WcxNode } from '@repo/ui/types/nodes'; @@ -19,7 +20,7 @@ import { WcxNode } from '@repo/ui/types/nodes'; /** * 노드 타입별 아이콘 매핑 */ -const NODE_TYPE_ICONS: Record = { +const NODE_TYPE_ICONS: Record = { Text: Type, Image: ImageIcon, Heading: Heading1, @@ -62,9 +63,9 @@ const LayerItem = ({ node, nodes, selectedId, onSelect, depth }: LayerItemProps) */ const getNodeName = () => { if ('props' in node) { - const props = node.props as any; - if (props.text) return props.text; - if (props.alt) return props.alt; + const props = node.props as Record; + if (props.text) return props.text as string; + if (props.alt) return props.alt as string; } return node.type; }; diff --git a/apps/editor/src/widgets/right-sidebar/ui/RightSidebar.tsx b/apps/editor/src/widgets/right-sidebar/ui/RightSidebar.tsx index 591fcda..d4840fc 100644 --- a/apps/editor/src/widgets/right-sidebar/ui/RightSidebar.tsx +++ b/apps/editor/src/widgets/right-sidebar/ui/RightSidebar.tsx @@ -12,6 +12,7 @@ import PropertySection from "./sections/PropertySection"; import { WcxNode } from "@repo/ui/types/nodes"; // 노드 타입별 Content 패널 매핑 +// eslint-disable-next-line @typescript-eslint/no-explicit-any const CONTENT_PANEL: Partial>> = { Text: TextPanel, Heading: TextPanel, diff --git a/apps/editor/src/widgets/right-sidebar/ui/panels/ButtonPanel.tsx b/apps/editor/src/widgets/right-sidebar/ui/panels/ButtonPanel.tsx index 12a88a3..ad77e53 100644 --- a/apps/editor/src/widgets/right-sidebar/ui/panels/ButtonPanel.tsx +++ b/apps/editor/src/widgets/right-sidebar/ui/panels/ButtonPanel.tsx @@ -13,7 +13,7 @@ interface ButtonPanelProps { export default function ButtonPanel({ node }: ButtonPanelProps) { const updateNode = useUpdateNode(); - const handlePropChange = (key: string, value: any) => { + const handlePropChange = (key: string, value: string | number | boolean | object) => { updateNode(node.id, { props: { ...node.props, [key]: value } }); }; diff --git a/apps/editor/src/widgets/right-sidebar/ui/panels/ImagePanel.tsx b/apps/editor/src/widgets/right-sidebar/ui/panels/ImagePanel.tsx index d8d1631..ebf5e73 100644 --- a/apps/editor/src/widgets/right-sidebar/ui/panels/ImagePanel.tsx +++ b/apps/editor/src/widgets/right-sidebar/ui/panels/ImagePanel.tsx @@ -17,7 +17,7 @@ export default function ImagePanel({ node }: ImagePanelProps) {
updateNode(node.id, { props: { ...node.props, alt: val } }) } @@ -25,6 +25,6 @@ export default function ImagePanel({ node }: ImagePanelProps) { /> -
+
); } diff --git a/apps/editor/src/widgets/right-sidebar/ui/panels/TextPanel.tsx b/apps/editor/src/widgets/right-sidebar/ui/panels/TextPanel.tsx index 88b57e3..f66f55a 100644 --- a/apps/editor/src/widgets/right-sidebar/ui/panels/TextPanel.tsx +++ b/apps/editor/src/widgets/right-sidebar/ui/panels/TextPanel.tsx @@ -18,11 +18,11 @@ interface TextPanelProps { export default function TextPanel({ node }: TextPanelProps) { const updateNode = useUpdateNode(); - const handlePropChange = (key: string, value: any) => { + const handlePropChange = (key: string, value: string | number | boolean | object) => { updateNode(node.id, { props: { ...node.props, [key]: value } }); }; - const handleStyleChange = (key: string, value: any) => { + const handleStyleChange = (key: string, value: string | number | boolean | object) => { updateNode(node.id, { style: { ...node.style, [key]: value } }); }; diff --git a/apps/editor/src/widgets/right-sidebar/ui/sections/LayoutSection.tsx b/apps/editor/src/widgets/right-sidebar/ui/sections/LayoutSection.tsx index a4b43ac..e8ddc15 100644 --- a/apps/editor/src/widgets/right-sidebar/ui/sections/LayoutSection.tsx +++ b/apps/editor/src/widgets/right-sidebar/ui/sections/LayoutSection.tsx @@ -16,7 +16,7 @@ interface LayoutSectionProps { export default function LayoutSection({ node }: LayoutSectionProps) { const updateNode = useUpdateNode(); - const handleStyleChange = (key: string, value: any) => { + const handleStyleChange = (key: string, value: string | number) => { updateNode(node.id, { style: { ...node.style, [key]: value } }); }; diff --git a/apps/editor/src/widgets/right-sidebar/ui/sections/PositionSection.tsx b/apps/editor/src/widgets/right-sidebar/ui/sections/PositionSection.tsx index 030d1b1..04ed4ba 100644 --- a/apps/editor/src/widgets/right-sidebar/ui/sections/PositionSection.tsx +++ b/apps/editor/src/widgets/right-sidebar/ui/sections/PositionSection.tsx @@ -17,11 +17,11 @@ export default function PositionSection({ node }: PositionSectionProps) { const updateNodeLayout = useUpdateNodeLayout(); const positionType = (node.style.position as PositionType) || "relative"; - const handleStyleChange = (key: string, value: any) => { + const handleStyleChange = (key: string, value: string | number | undefined) => { updateNode(node.id, { style: { ...node.style, [key]: value } }); }; - const handleLayoutChange = (key: string, value: any) => { + const handleLayoutChange = (key: string, value: string | number) => { updateNodeLayout(node.id, { [key]: value }); }; diff --git a/apps/editor/src/widgets/right-sidebar/ui/sections/PropertySection.tsx b/apps/editor/src/widgets/right-sidebar/ui/sections/PropertySection.tsx index 37767c9..87e92d7 100644 --- a/apps/editor/src/widgets/right-sidebar/ui/sections/PropertySection.tsx +++ b/apps/editor/src/widgets/right-sidebar/ui/sections/PropertySection.tsx @@ -1,6 +1,6 @@ "use client"; -import { cn } from "@repo/utils"; + import { Minus, Plus } from "lucide-react"; import { useState } from "react"; diff --git a/apps/editor/src/widgets/right-sidebar/ui/sections/SizeSection.tsx b/apps/editor/src/widgets/right-sidebar/ui/sections/SizeSection.tsx index 3b33178..fa3286c 100644 --- a/apps/editor/src/widgets/right-sidebar/ui/sections/SizeSection.tsx +++ b/apps/editor/src/widgets/right-sidebar/ui/sections/SizeSection.tsx @@ -17,8 +17,8 @@ export default function SizeSection({ node }: SizeSectionProps) { const updateNode = useUpdateNode(); const nodes = useCurNodes(); - const widthMode: SizingMode = (node.layout as any).widthMode || "fixed"; - const heightMode: SizingMode = (node.layout as any).heightMode || "fixed"; + const widthMode: SizingMode = (node.layout as { widthMode: SizingMode }).widthMode || "fixed"; + const heightMode: SizingMode = (node.layout as { heightMode: SizingMode }).heightMode || "fixed"; const widthValue = typeof node.layout.width === "number" ? node.layout.width @@ -28,7 +28,7 @@ export default function SizeSection({ node }: SizeSectionProps) { ? node.layout.height : parseInt(String(node.layout.height)) || 0; - const handleStyleChange = (key: string, value: any) => { + const handleStyleChange = (key: string, value: string | number) => { updateNode(node.id, { style: { ...node.style, [key]: value } }); }; @@ -65,6 +65,7 @@ export default function SizeSection({ node }: SizeSectionProps) { const oldValue = isHeight ? heightValue : widthValue; const newValue = convertValue(oldValue, oldMode, newMode, isHeight); + // eslint-disable-next-line @typescript-eslint/no-explicit-any updateLayout(node.id, { [key]: newMode, [sizeKey]: newValue } as any); }; From b44866ac70f1381880155bd9f73a2475a2920df1 Mon Sep 17 00:00:00 2001 From: "DESKTOP-4N4R3LH\\jqk17" Date: Fri, 20 Feb 2026 17:15:02 +0900 Subject: [PATCH 14/15] =?UTF-8?q?=F0=9F=90=9Bfix:=20CI=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/editor/src/app/editor/page.tsx | 202 +---------------------- apps/editor/src/stores/useEditorStore.ts | 2 +- 2 files changed, 7 insertions(+), 197 deletions(-) diff --git a/apps/editor/src/app/editor/page.tsx b/apps/editor/src/app/editor/page.tsx index 030c7db..e48fc65 100644 --- a/apps/editor/src/app/editor/page.tsx +++ b/apps/editor/src/app/editor/page.tsx @@ -7,206 +7,16 @@ import { RuntimeProvider } from "@repo/ui/context/runtimeContext"; export default async function EditorPage() { const pageId = 201; //목데이터입니다. - // 중첩 구조 실험을 위한 테스트 데이터 - // 레이어 기능을 테스트하기 위한 복잡한 중첩 구조 데이터 - const nodes: any[] = [ - { - id: "root-container", - type: "Stack", - page_id: pageId, - parent_id: null, - position: 1, - style: { - display: "flex", - flexDirection: "column", - backgroundColor: "#ffffff", - padding: "0px", - gap: "0px", - borderRadius: "0px", - position: "absolute", - overflow: "hidden", - border: "1px solid #e5e7eb", - boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1)", - }, - layout: { - x: 50, y: 50, width: 800, height: 600, zIndex: 1, - widthMode: "fixed", heightMode: "fixed", - widthUnit: "px", heightUnit: "px" - } - }, - // --- Navigation Bar --- - { - id: "navbar", - type: "Stack", - page_id: pageId, - parent_id: "root-container", - position: 1, - style: { - display: "flex", - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - padding: "0 24px", - height: "64px", - backgroundColor: "#ffffff", - borderBottom: "1px solid #f3f4f6", - }, - layout: { - x: 0, y: 0, width: "100%", height: 64, zIndex: 1, - widthMode: "fill", heightMode: "fixed", - widthUnit: "%", heightUnit: "px" - } - }, - { - id: "logo", - type: "Text", - page_id: pageId, - parent_id: "navbar", - position: 1, - props: { text: "WCX Editor", level: "h4" }, - style: { fontWeight: "900", color: "#111827", fontSize: "18px" }, - layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } - }, - { - id: "nav-links", - type: "Stack", - page_id: pageId, - parent_id: "navbar", - position: 2, - style: { display: "flex", flexDirection: "row", gap: "20px", alignItems: "center" }, - layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } - }, - { - id: "link-home", - type: "Text", - page_id: pageId, - parent_id: "nav-links", - position: 1, - props: { text: "Home", level: "h6" }, - style: { color: "#4b5563", fontSize: "14px" }, - layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } - }, - { - id: "link-about", - type: "Text", - page_id: pageId, - parent_id: "nav-links", - position: 2, - props: { text: "About", level: "h6" }, - style: { color: "#4b5563", fontSize: "14px" }, - layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } - }, - // --- Hero Section --- - { - id: "hero-section", - type: "Stack", - page_id: pageId, - parent_id: "root-container", - position: 2, - style: { - display: "flex", - flexDirection: "column", - alignItems: "center", - justifyContent: "center", - flex: 1, - padding: "60px 20px", - backgroundColor: "#f9fafb", - textAlign: "center" - }, - layout: { - x: 0, y: 0, width: "100%", height: "auto", zIndex: 1, - widthMode: "fill", heightMode: "fill", - widthUnit: "%", heightUnit: "px" - } - }, - { - id: "hero-title", - type: "Heading", - page_id: pageId, - parent_id: "hero-section", - position: 1, - props: { text: "Build Your Web App Faster", level: "h1" }, - style: { color: "#111827", fontWeight: "800", marginBottom: "16px", fontSize: "36px" }, - layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } - }, - { - id: "hero-desc", - type: "Text", - page_id: pageId, - parent_id: "hero-section", - position: 2, - props: { text: "The most powerful no-code editor for modern web developers.", level: "h5" }, - style: { color: "#6b7280", maxWidth: "500px", marginBottom: "32px" }, - layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } - }, - { - id: "hero-cta-container", - type: "Stack", - page_id: pageId, - parent_id: "hero-section", - position: 3, - style: { display: "flex", flexDirection: "row", gap: "12px" }, - layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } - }, - { - id: "btn-primary", - type: "Button", - page_id: pageId, - parent_id: "hero-cta-container", - position: 1, - props: { text: "Get Started" }, - style: { backgroundColor: "#000000", color: "#ffffff", padding: "12px 24px", borderRadius: "8px", fontWeight: "600" }, - layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } - }, - { - id: "btn-secondary", - type: "Button", - page_id: pageId, - parent_id: "hero-cta-container", - position: 2, - props: { text: "View Demo" }, - style: { backgroundColor: "#ffffff", color: "#000000", padding: "12px 24px", borderRadius: "8px", border: "1px solid #e5e7eb", fontWeight: "600" }, - layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } - }, - // --- Footer --- - { - id: "footer", - type: "Stack", - page_id: pageId, - parent_id: "root-container", - position: 3, - style: { - display: "flex", - flexDirection: "row", - alignItems: "center", - justifyContent: "center", - padding: "20px", - backgroundColor: "#ffffff", - borderTop: "1px solid #f3f4f6" - }, - layout: { - x: 0, y: 0, width: "100%", height: 60, zIndex: 1, - widthMode: "fill", heightMode: "fixed", - widthUnit: "%", heightUnit: "px" - } - }, - { - id: "footer-text", - type: "Text", - page_id: pageId, - parent_id: "footer", - position: 1, - props: { text: "© 2024 WebCreatorX. All rights reserved.", level: "h6" }, - style: { color: "#9ca3af", fontSize: "12px" }, - layout: { x: 0, y: 0, width: "auto", height: "auto", zIndex: 1, widthMode: "fit", heightMode: "fit", widthUnit: "px", heightUnit: "px" } - } - ]; + //서버 액션 함수(nodes데이터 패칭함수) + const nodes = await getNodesFromDB(pageId); return ( -
+
+

에디터 페이지 입니다.

+ {/* 다른 에디터 관련 컴포넌트들은 이곳에서 렌더링 됩니다!(사이드바,매니패스트 수정 컴포넌트 등등...) */} -
+
diff --git a/apps/editor/src/stores/useEditorStore.ts b/apps/editor/src/stores/useEditorStore.ts index 2d6f21e..848d58c 100644 --- a/apps/editor/src/stores/useEditorStore.ts +++ b/apps/editor/src/stores/useEditorStore.ts @@ -128,7 +128,7 @@ const useEditorStore = create( // 1. 최상위 속성 업데이트 (type 등) // (주의: 객체 타입인 style, props, layout을 통째로 덮어쓰지 않도록 별도 처리) - const { style, props, layout: _layout, ...rest } = updates; + const { style, props, layout, ...rest } = updates; Object.assign(targetNode, rest); // 2. 하위 객체 병합 업데이트 From b4ef72dc26df301a1452078e8209a75e06f0b955 Mon Sep 17 00:00:00 2001 From: "DESKTOP-4N4R3LH\\jqk17" Date: Fri, 20 Feb 2026 17:17:53 +0900 Subject: [PATCH 15/15] =?UTF-8?q?=F0=9F=90=9Bfix:=20CI=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/editor/src/shared/lib/component-defaults.ts | 15 +++++++++++++++ apps/editor/src/stores/useEditorStore.ts | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/editor/src/shared/lib/component-defaults.ts b/apps/editor/src/shared/lib/component-defaults.ts index cb93591..1544c27 100644 --- a/apps/editor/src/shared/lib/component-defaults.ts +++ b/apps/editor/src/shared/lib/component-defaults.ts @@ -17,6 +17,13 @@ const DEFAULT_FLEX_STYLE: NodeStyle = { alignItems: 'center', }; +const DEFAULT_LAYOUT_MODE = { + widthMode: 'fixed' as const, + heightMode: 'fixed' as const, + widthUnit: 'px' as const, + heightUnit: 'px' as const, +}; + export const COMPONENT_DEFAULTS: Record = { Image: { props: { @@ -33,6 +40,7 @@ export const COMPONENT_DEFAULTS: Record = { width: 400, height: 300, zIndex: 0, + ...DEFAULT_LAYOUT_MODE, }, }, Heading: { @@ -52,6 +60,7 @@ export const COMPONENT_DEFAULTS: Record = { width: 200, height: 50, zIndex: 1, + ...DEFAULT_LAYOUT_MODE, }, }, Text: { @@ -72,6 +81,7 @@ export const COMPONENT_DEFAULTS: Record = { width: 300, height: 100, zIndex: 1, + ...DEFAULT_LAYOUT_MODE, }, }, Button: { @@ -90,6 +100,7 @@ export const COMPONENT_DEFAULTS: Record = { width: 120, height: 40, zIndex: 2, + ...DEFAULT_LAYOUT_MODE, }, }, Container: { @@ -107,6 +118,7 @@ export const COMPONENT_DEFAULTS: Record = { width: 500, height: 200, zIndex: 0, + ...DEFAULT_LAYOUT_MODE, }, }, Modal: { @@ -127,6 +139,7 @@ export const COMPONENT_DEFAULTS: Record = { width: 400, height: 300, zIndex: 100, + ...DEFAULT_LAYOUT_MODE, }, }, Stack: { @@ -144,6 +157,7 @@ export const COMPONENT_DEFAULTS: Record = { width: 300, height: 300, zIndex: 0, + ...DEFAULT_LAYOUT_MODE, }, }, Group: { @@ -157,6 +171,7 @@ export const COMPONENT_DEFAULTS: Record = { width: 200, height: 200, zIndex: 0, + ...DEFAULT_LAYOUT_MODE, }, }, }; diff --git a/apps/editor/src/stores/useEditorStore.ts b/apps/editor/src/stores/useEditorStore.ts index 848d58c..f034a32 100644 --- a/apps/editor/src/stores/useEditorStore.ts +++ b/apps/editor/src/stores/useEditorStore.ts @@ -128,7 +128,8 @@ const useEditorStore = create( // 1. 최상위 속성 업데이트 (type 등) // (주의: 객체 타입인 style, props, layout을 통째로 덮어쓰지 않도록 별도 처리) - const { style, props, layout, ...rest } = updates; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { style, props, layout: _layout, ...rest } = updates; Object.assign(targetNode, rest); // 2. 하위 객체 병합 업데이트