이미지 파일 업로드 기능
This commit is contained in:
+96
-6
@@ -38,6 +38,7 @@ import type {
|
||||
FormCheckboxBlockProps,
|
||||
FormRadioBlockProps,
|
||||
ListItemNode,
|
||||
ProjectConfig,
|
||||
} from "@/features/editor/state/editorStore";
|
||||
import { ButtonPropertiesPanel } from "./panels/ButtonPropertiesPanel";
|
||||
import { TextPropertiesPanel } from "./panels/TextPropertiesPanel";
|
||||
@@ -95,6 +96,9 @@ export default function EditorPage() {
|
||||
const redo = useEditorStore((state) => state.redo);
|
||||
const removeBlock = useEditorStore((state) => state.removeBlock);
|
||||
const duplicateBlock = useEditorStore((state) => state.duplicateBlock);
|
||||
const projectConfig = useEditorStore(
|
||||
(state) => (state as any).projectConfig as ProjectConfig,
|
||||
);
|
||||
|
||||
const [editingBlockId, setEditingBlockId] = useState<string | null>(null);
|
||||
const [editingText, setEditingText] = useState("");
|
||||
@@ -115,12 +119,53 @@ export default function EditorPage() {
|
||||
|
||||
const sensors = useSensors(useSensor(PointerSensor));
|
||||
|
||||
const editorMainStyle: CSSProperties = {};
|
||||
if (projectConfig.bodyBgColorHex && projectConfig.bodyBgColorHex.trim()) {
|
||||
editorMainStyle.backgroundColor = projectConfig.bodyBgColorHex;
|
||||
}
|
||||
|
||||
const startEditing = (id: string, initialText: string) => {
|
||||
selectBlock(id);
|
||||
setEditingBlockId(id);
|
||||
setEditingText(initialText);
|
||||
};
|
||||
|
||||
const handleExportZip = async () => {
|
||||
try {
|
||||
const payload = {
|
||||
blocks,
|
||||
projectConfig,
|
||||
};
|
||||
|
||||
const response = await fetch("/api/export", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("정적 파일 내보내기 실패", await response.text().catch(() => ""));
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const slug = (projectConfig.slug ?? "").trim() || "page-builder-export";
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `${slug}.zip`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error("정적 파일 내보내기 중 오류", error);
|
||||
}
|
||||
};
|
||||
|
||||
const commitEditing = () => {
|
||||
if (!editingBlockId) return;
|
||||
updateBlock(editingBlockId, { text: editingText });
|
||||
@@ -545,7 +590,7 @@ export default function EditorPage() {
|
||||
});
|
||||
|
||||
return (
|
||||
<main className="min-h-screen flex flex-col bg-slate-950 text-slate-50">
|
||||
<main className="min-h-screen flex flex-col" style={editorMainStyle}>
|
||||
<header className="border-b border-slate-800 px-6 py-4 flex items-center justify-between relative z-20">
|
||||
<div className="flex items-baseline gap-3">
|
||||
<h1 className="text-xl font-semibold">Page Editor</h1>
|
||||
@@ -589,6 +634,16 @@ export default function EditorPage() {
|
||||
>
|
||||
JSON 내보내기/불러오기
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full px-3 py-2 text-left hover:bg-slate-800 text-slate-100 border-t border-slate-800"
|
||||
onClick={() => {
|
||||
setMenuOpen(false);
|
||||
void handleExportZip();
|
||||
}}
|
||||
>
|
||||
페이지 파일로 내보내기 (ZIP)
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full px-3 py-2 text-left hover:bg-red-900/60 text-red-100 border-t border-slate-800"
|
||||
@@ -621,6 +676,7 @@ export default function EditorPage() {
|
||||
handleDragStart={handleDragStart}
|
||||
handleDragEnd={handleDragEnd}
|
||||
handleDragCancel={handleDragCancel}
|
||||
projectConfig={projectConfig}
|
||||
/>
|
||||
<PropertiesSidebar
|
||||
blocks={blocks}
|
||||
@@ -1232,7 +1288,7 @@ function SortableEditorBlock({
|
||||
<img
|
||||
src={radioProps.groupLabelImageUrl}
|
||||
alt={radioProps.groupLabel || "폼 그룹 타이틀"}
|
||||
className="h-4 w-auto text-slate-200"
|
||||
className="inline-block max-w-full h-auto"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-slate-200">{radioProps.groupLabel}</span>
|
||||
@@ -1309,7 +1365,7 @@ function SortableEditorBlock({
|
||||
<img
|
||||
src={checkboxProps.groupLabelImageUrl}
|
||||
alt={checkboxProps.groupLabel || "폼 그룹 타이틀"}
|
||||
className="h-4 w-auto text-slate-200"
|
||||
className="inline-block max-w-full h-auto"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-slate-200">{checkboxProps.groupLabel}</span>
|
||||
@@ -1416,18 +1472,52 @@ function SortableEditorBlock({
|
||||
{block.type === "image" && (() => {
|
||||
const imageProps = block.props as ImageBlockProps;
|
||||
const hasSrc = imageProps.src.trim().length > 0;
|
||||
const align = imageProps.align ?? "center";
|
||||
const alignClass =
|
||||
align === "left" ? "justify-start" : align === "right" ? "justify-end" : "justify-center";
|
||||
|
||||
const containerStyle: React.CSSProperties = {};
|
||||
const widthMode = imageProps.widthMode ?? "auto";
|
||||
if (widthMode === "fixed" && typeof imageProps.widthPx === "number" && imageProps.widthPx > 0) {
|
||||
containerStyle.width = `${imageProps.widthPx}px`;
|
||||
}
|
||||
|
||||
const imageStyle: React.CSSProperties = {
|
||||
maxWidth: "100%",
|
||||
height: "auto",
|
||||
};
|
||||
const radiusToken = imageProps.borderRadius ?? "md";
|
||||
const fallbackRadiusPx =
|
||||
radiusToken === "none"
|
||||
? 0
|
||||
: radiusToken === "sm"
|
||||
? 4
|
||||
: radiusToken === "lg"
|
||||
? 16
|
||||
: radiusToken === "full"
|
||||
? 9999
|
||||
: 8;
|
||||
const radiusPx =
|
||||
typeof imageProps.borderRadiusPx === "number" && imageProps.borderRadiusPx >= 0
|
||||
? imageProps.borderRadiusPx
|
||||
: fallbackRadiusPx;
|
||||
imageStyle.borderRadius = `${radiusPx}px`;
|
||||
|
||||
return (
|
||||
<div className="w-full border border-dashed border-slate-700 bg-slate-900/40 rounded flex items-center justify-center overflow-hidden">
|
||||
<div
|
||||
className={`w-full border border-dashed border-slate-700 bg-slate-900/40 rounded flex items-center overflow-hidden ${alignClass}`}
|
||||
>
|
||||
{hasSrc ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={imageProps.src}
|
||||
alt={imageProps.alt}
|
||||
className="max-w-full h-auto object-contain"
|
||||
className="object-contain"
|
||||
style={{ ...containerStyle, ...imageStyle }}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-[10px] text-slate-500 px-2 py-4">
|
||||
이미지 URL을 속성 패널에서 입력하세요.
|
||||
이미지 URL 을 입력하거나 파일을 업로드하면 여기에서 미리보기가 표시됩니다.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user