73 lines
2.3 KiB
TypeScript
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 />;
|
||
|
|
};
|