Files
ef_front/app/components/editor.tsx
2025-09-07 22:57:43 +00:00

73 lines
2.3 KiB
TypeScript

"use client";
import type { FC } from "react";
import { Crepe as CrepeClass } from "@milkdown/crepe";
import { Milkdown, useEditor } from "@milkdown/react";
import { Decoration } from "@milkdown/prose/view";
import { upload, uploadConfig, type Uploader } from "@milkdown/plugin-upload";
import type { Node as PMNode } from "@milkdown/prose/model";
// (선택) UI 스타일
import "@milkdown/crepe/theme/common/style.css";
import "@milkdown/crepe/theme/frame.css";
async function uploadToServer(file: File): Promise<string> {
const fd = new FormData();
fd.append("file", file);
const res = await fetch("/api/upload", { method: "POST", body: fd });
const data = await res.json();
if (!res.ok || !data.ok) throw new Error(data?.error ?? "UPLOAD_FAILED");
return data.url as string; // 예: /uploads/xxx.png
}
export const MilkdownEditor: FC<{
editorRef?: React.RefObject<CrepeClass>;
}> = ({ editorRef }) => {
useEditor((root) => {
const crepe = new CrepeClass({
root,
defaultValue: "여기에 내용을 입력",
featureConfigs: {
[CrepeClass.Feature.ImageBlock]: {
// ✅ 이 버전에선 string(URL)만 반환
onUpload: async (file: File) => {
const url = await uploadToServer(file);
return url;
},
},
},
});
if (editorRef) editorRef.current = crepe;
const uploader: Uploader = async (files, schema) => {
const nodes: PMNode[] = [];
for (let i = 0; i < files.length; i++) {
const f = files.item(i);
if (!f || !f.type.startsWith("image/")) continue;
const url = await uploadToServer(f);
const node = schema.nodes.image.createAndFill({ src: url, alt: f.name, title: "" });
if (node) nodes.push(node);
}
return nodes;
};
crepe.editor
.config((ctx) => {
ctx.set(uploadConfig.key, {
enableHtmlFileUploader: true,
uploadWidgetFactory: (pos) =>
Decoration.widget(pos, () => {
const el = document.createElement("span");
el.className = "md-upload-placeholder";
return el;
}),
uploader, // 붙여넣기/드래그 업로드
});
})
.use(upload);
return crepe.editor;
}, []);
return <Milkdown />;
};