159 lines
4.8 KiB
TypeScript
159 lines
4.8 KiB
TypeScript
"use client";
|
|
|
|
import type { CSSProperties, ReactNode } from "react";
|
|
import {
|
|
DndContext,
|
|
DragOverlay,
|
|
type DragEndEvent,
|
|
type DragStartEvent,
|
|
} from "@dnd-kit/core";
|
|
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
import { rectIntersection } from "@dnd-kit/core";
|
|
|
|
import type { Block, ButtonBlockProps, ImageBlockProps, ProjectConfig, TextBlockProps } from "@/features/editor/state/editorStore";
|
|
|
|
interface EditorCanvasProps {
|
|
blocks: Block[];
|
|
rootBlocks: Block[];
|
|
selectedBlockId: string | null;
|
|
editingBlockId: string | null;
|
|
editingText: string;
|
|
activeDragId: string | null;
|
|
sensors: any;
|
|
onCanvasEmptyClick: () => void;
|
|
renderBlocks: (targetBlocks: Block[]) => ReactNode;
|
|
handleDragStart: (event: DragStartEvent) => void;
|
|
handleDragEnd: (event: DragEndEvent) => void;
|
|
handleDragCancel: () => void;
|
|
projectConfig: ProjectConfig;
|
|
}
|
|
|
|
export function EditorCanvas(props: EditorCanvasProps) {
|
|
const {
|
|
blocks,
|
|
rootBlocks,
|
|
activeDragId,
|
|
sensors,
|
|
onCanvasEmptyClick,
|
|
renderBlocks,
|
|
handleDragStart,
|
|
handleDragEnd,
|
|
handleDragCancel,
|
|
projectConfig,
|
|
} = props;
|
|
|
|
const canvasOuterStyle: CSSProperties = {};
|
|
const canvasInnerStyle: CSSProperties = {};
|
|
const preset = projectConfig.canvasPreset ?? "full";
|
|
const widthPx =
|
|
typeof projectConfig.canvasWidthPx === "number" && projectConfig.canvasWidthPx > 0
|
|
? projectConfig.canvasWidthPx
|
|
: null;
|
|
|
|
if (widthPx != null) {
|
|
canvasInnerStyle.maxWidth = `${widthPx}px`;
|
|
} else if (preset === "mobile") {
|
|
canvasInnerStyle.maxWidth = "390px";
|
|
} else if (preset === "tablet") {
|
|
canvasInnerStyle.maxWidth = "768px";
|
|
} else if (preset === "desktop") {
|
|
canvasInnerStyle.maxWidth = "1200px";
|
|
}
|
|
|
|
if (projectConfig.bodyBgColorHex && projectConfig.bodyBgColorHex.trim()) {
|
|
canvasOuterStyle.backgroundColor = projectConfig.bodyBgColorHex;
|
|
}
|
|
|
|
if (projectConfig.canvasBgColorHex && projectConfig.canvasBgColorHex.trim()) {
|
|
canvasInnerStyle.backgroundColor = projectConfig.canvasBgColorHex;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="flex-1 p-4 flex flex-col gap-2 text-sm border-r border-slate-800 overflow-auto pb-scroll"
|
|
data-testid="editor-canvas"
|
|
style={canvasOuterStyle}
|
|
onClick={(event) => {
|
|
if (event.target === event.currentTarget) {
|
|
onCanvasEmptyClick();
|
|
}
|
|
}}
|
|
>
|
|
<div
|
|
data-testid="editor-canvas-inner"
|
|
className="mx-auto w-full flex flex-col gap-2"
|
|
style={canvasInnerStyle}
|
|
>
|
|
{blocks.length === 0 ? (
|
|
<div className="flex-1 flex items-center justify-center text-slate-500 text-xs">
|
|
왼쪽에서 "텍스트" 버튼을 눌러 블록을 추가해 보세요.
|
|
</div>
|
|
) : (
|
|
<DndContext
|
|
sensors={sensors}
|
|
collisionDetection={rectIntersection}
|
|
onDragStart={handleDragStart}
|
|
onDragEnd={handleDragEnd}
|
|
onDragCancel={handleDragCancel}
|
|
>
|
|
<SortableContext
|
|
items={rootBlocks.map((b) => b.id)}
|
|
strategy={verticalListSortingStrategy}
|
|
>
|
|
{renderBlocks(rootBlocks)}
|
|
</SortableContext>
|
|
<DragOverlay>
|
|
{activeDragId && (
|
|
<DragPreview block={blocks.find((b) => b.id === activeDragId) ?? null} />
|
|
)}
|
|
</DragOverlay>
|
|
</DndContext>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface DragPreviewProps {
|
|
block: Block | null;
|
|
}
|
|
|
|
export function EditorCanvasDragPreview({ block }: DragPreviewProps) {
|
|
if (!block) return null;
|
|
|
|
return (
|
|
<div className="pointer-events-none rounded border border-sky-500 bg-white px-3 py-2 text-xs text-slate-900 shadow-lg opacity-90 min-w-[160px] max-w-[260px] dark:bg-slate-900 dark:text-slate-100">
|
|
<div className="text-[10px] uppercase tracking-wide text-sky-300 mb-1">
|
|
{block.type === "text"
|
|
? "Text"
|
|
: block.type === "button"
|
|
? "Button"
|
|
: block.type === "image"
|
|
? "Image"
|
|
: block.type === "divider"
|
|
? "Divider"
|
|
: block.type === "list"
|
|
? "List"
|
|
: "Section"}
|
|
</div>
|
|
<div className="truncate">
|
|
{block.type === "text"
|
|
? (block.props as TextBlockProps).text || "텍스트 블록"
|
|
: block.type === "button"
|
|
? (block.props as ButtonBlockProps).label || "버튼 블록"
|
|
: block.type === "image"
|
|
? (block.props as ImageBlockProps).alt || "이미지 블록"
|
|
: block.type === "list"
|
|
? "리스트 블록"
|
|
: block.type === "divider"
|
|
? "구분선 블록"
|
|
: "섹션 블록"}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DragPreview({ block }: DragPreviewProps) {
|
|
return <EditorCanvasDragPreview block={block} />;
|
|
}
|