"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 { 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; }> = ({ 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 ; };