"use client"; import { useEffect, useRef, useCallback } from "react"; import { useToast } from "@/app/components/ui/ToastProvider"; type Props = { value: string; onChange: (v: string) => void; placeholder?: string }; async function readFileAsDataUrl(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result)); reader.onerror = reject; reader.readAsDataURL(file); }); } async function resizeImageToBlob(dataUrl: string, opts: { maxWidth: number; maxHeight: number; quality: number }): Promise { const img = document.createElement("img"); img.decoding = "async"; img.loading = "eager"; img.src = dataUrl; await new Promise((res, rej) => { img.onload = () => res(null); img.onerror = rej; }); const w = img.naturalWidth || 1; const h = img.naturalHeight || 1; const scale = Math.min(1, opts.maxWidth / w || 1, opts.maxHeight / h || 1); const targetW = Math.max(1, Math.round(w * scale)); const targetH = Math.max(1, Math.round(h * scale)); const canvas = document.createElement("canvas"); canvas.width = targetW; canvas.height = targetH; const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Canvas unsupported"); ctx.drawImage(img, 0, 0, targetW, targetH); return await new Promise((resolve) => canvas.toBlob((b) => resolve(b as Blob), "image/webp", opts.quality)); } export function Editor({ value, onChange, placeholder }: Props) { const ref = useRef(null); const { show } = useToast(); useEffect(() => { const el = ref.current; if (!el) return; if (el.innerHTML !== value) el.innerHTML = value || ""; }, [value]); const insertHtmlAtCursor = useCallback((html: string) => { // eslint-disable-next-line deprecation/deprecation document.execCommand("insertHTML", false, html); }, []); const uploadAndInsert = useCallback( async (files: File[]) => { const images = files.filter((f) => f.type.startsWith("image/")); if (images.length === 0) return; for (const f of images) { try { const dataUrl = await readFileAsDataUrl(f); const blob = await resizeImageToBlob(dataUrl, { maxWidth: 1600, maxHeight: 1600, quality: 0.9 }); const fd = new FormData(); fd.append("file", new File([blob], `${Date.now()}.webp`, { type: "image/webp" })); const r = await fetch("/api/uploads", { method: "POST", body: fd }); const data = await r.json(); if (!r.ok) throw new Error(JSON.stringify(data)); const alt = window.prompt("이미지 대체텍스트(ALT)를 입력하세요.") || ""; const caption = window.prompt("이미지 캡션(선택)") || ""; const figure = `
${alt.replace(/
${caption.replace(/
`; insertHtmlAtCursor(figure); // onChange 동기화 const el = ref.current; if (el) onChange(el.innerHTML); } catch { show("이미지 업로드 실패"); } } }, [insertHtmlAtCursor, onChange, show] ); const onPaste = useCallback( async (e: React.ClipboardEvent) => { const files = Array.from(e.clipboardData?.files ?? []); if (files.some((f) => f.type.startsWith("image/"))) { e.preventDefault(); await uploadAndInsert(files); } }, [uploadAndInsert] ); const onDrop = useCallback( async (e: React.DragEvent) => { e.preventDefault(); const files = Array.from(e.dataTransfer?.files ?? []); if (files.length > 0) await uploadAndInsert(files); }, [uploadAndInsert] ); return (
onChange((e.target as HTMLDivElement).innerHTML)} onPaste={onPaste} onDrop={onDrop} onDragOver={(e) => e.preventDefault()} data-placeholder={placeholder} style={{ minHeight: 160, border: "1px solid #ddd", borderRadius: 6, padding: 12, }} suppressContentEditableWarning /> ); }