import { useEditor, EditorContent } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit' import Underline from '@tiptap/extension-underline' import { Image as TiptapImage } from '@tiptap/extension-image' import Link from '@tiptap/extension-link' import TextAlign from '@tiptap/extension-text-align' import Subscript from '@tiptap/extension-subscript' import Superscript from '@tiptap/extension-superscript' import Table from '@tiptap/extension-table' import TableRow from '@tiptap/extension-table-row' import TableCell from '@tiptap/extension-table-cell' import TableHeader from '@tiptap/extension-table-header' import Highlight from '@tiptap/extension-highlight' import TaskList from '@tiptap/extension-task-list' import TaskItem from '@tiptap/extension-task-item' import BulletList from '@tiptap/extension-bullet-list' import ListItem from '@tiptap/extension-list-item' import OrderedList from '@tiptap/extension-ordered-list' import Blockquote from '@tiptap/extension-blockquote' import { Toolbar } from './richTextEditor' interface TiptapEditorProps { content: string; setContent: (content: string) => void; disabled?: boolean; height?: string; // 높이 prop 추가 } export default function TiptapEditor({ content, setContent, disabled, height = "300px" }: TiptapEditorProps) { const editor = useEditor({ extensions: [ StarterKit.configure({ bulletList: false, orderedList: false, listItem: false, blockquote: false, codeBlock: false, code: false, heading: { levels: [1, 2, 3] } }), Underline, TiptapImage.configure({ HTMLAttributes: { class: 'max-w-full h-auto', style: 'max-width: 600px; height: auto;', }, }), Link.configure({ openOnClick: true, linkOnPaste: true, }), TextAlign.configure({ types: ['heading', 'paragraph'], alignments: ['left', 'center', 'right', 'justify'], defaultAlignment: 'left', }), Subscript, Superscript, Table.configure({ resizable: true, HTMLAttributes: { class: 'border-collapse table-auto w-full', }, }), TableRow.configure({ HTMLAttributes: { class: 'border-b border-gray-200', }, }), TableCell.configure({ HTMLAttributes: { class: 'border border-gray-200 p-2', }, }), TableHeader.configure({ HTMLAttributes: { class: 'border border-gray-200 p-2 bg-gray-100 font-bold', }, }), Highlight.configure({ multicolor: true, }), TaskList, TaskItem.configure({ nested: true, }), BulletList.configure({ HTMLAttributes: { class: 'list-disc ml-4', }, }), ListItem, OrderedList.configure({ HTMLAttributes: { class: 'list-decimal ml-4', }, }), Blockquote.configure({ HTMLAttributes: { class: 'border-l-4 border-gray-300 pl-4 my-4 italic bg-gray-50 py-2', }, }), ], content: content, editable: !disabled, editorProps: { attributes: { class: 'w-full bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 prose prose-sm max-w-none', }, handleDrop: (view, event, slice, moved) => { if (!moved && event.dataTransfer?.files.length) { const file = event.dataTransfer.files[0]; if (file.type.startsWith('image/')) { handleImageUpload(file); return true; } } return false; }, handlePaste: (view, event) => { if (event.clipboardData?.files.length) { const file = event.clipboardData.files[0]; if (file.type.startsWith('image/')) { handleImageUpload(file); return true; } } return false; }, }, onUpdate: ({ editor }) => { setContent(editor.getHTML()); }, }) // 이미지 크기 확인 함수 const getImageDimensions = (src: string): Promise<{ width: number; height: number }> => new Promise((resolve, reject) => { // 1. 올바른 생성자 const img = new Image(); // 2. 로딩 완료 시 naturalWidth/Height 사용 img.onload = () => { resolve({ width: img.naturalWidth, height: img.naturalHeight }); }; img.onerror = () => reject(new Error('이미지 로드 실패')); img.src = src; // 3. 마지막에 src 지정 }); async function uploadImageToServer(file: File): Promise { const formData = new FormData(); formData.append('file', file); const res = await fetch('/api/richtext', { method: 'POST', body: formData, }); if (!res.ok) { const { error } = await res.json(); throw new Error(error ?? 'upload failed'); } const { url } = (await res.json()) as { url: string }; return url; } // Base64 → 서버 업로드 방식으로 교체 const handleImageUpload = async (file: File) => { try { if (file.size > 3 * 1024 * 1024) { alert('이미지 크기는 3 MB 이하만 지원됩니다.'); return; } if (!file.type.startsWith('image/')) { alert('이미지 파일만 업로드 가능합니다.'); return; } const url = await uploadImageToServer(file); // ← 업로드 & URL 획득 // 이미지 크기(너비)에 따라 style 조정 const { width, height } = await getImageDimensions(url); const maxWidth = 600; const newW = width > maxWidth ? maxWidth : width; const newH = width > maxWidth ? (height * maxWidth) / width : height; editor ?.chain() .focus() .setImage({ src: url, style: `width:${newW}px;height:${newH}px;max-width:100%;`, }) .run(); } catch (e) { console.error(e); alert('이미지 업로드에 실패했습니다.'); } }; // 높이 계산 (100%인 경우 flex 사용, 아니면 구체적 높이) const containerStyle = height === "100%" ? { height: "100%" } : { height }; const editorContentStyle = height === "100%" ? { flex: 1, minHeight: 0 } : { height: `calc(${height} - 60px)` }; // 툴바 높이(60px) 제외 return (
{/* 툴바: 항상 고정 위치 */}
{ const file = e.target.files?.[0]; if (file) { handleImageUpload(file); } }} />
{/* 에디터 내용: 스크롤 가능 영역 */}
{/* 전역 이미지 스타일 */}
) }