'use client'; import { useEffect, useRef } from 'react'; import { Rnd } from 'react-rnd'; import Quill from 'quill'; import 'quill/dist/quill.snow.css'; import '@/styles/quill.scss'; import ReactDOM from 'react-dom/client'; import FileUploader from './FileUploader'; import FileBlot from './FileBlot'; const BlockEmbed = Quill.import('blots/block/embed'); // --------- FileUploader --------- class ResizableImageBlot extends BlockEmbed { static blotName = 'resizableImage'; static tagName = 'div'; static className = 'resizable-image-container'; static create(value) { // value: { url: string, width?: number, height?: number } const node = super.create(); // 저장할 이미지 정보 node.setAttribute('data-url', value.url); node.setAttribute('data-width', value.width || 200); node.setAttribute('data-height', value.height || 200); // React 컴포넌트를 렌더링할 컨테이너 생성 const container = document.createElement('div'); container.classList.add('resizable-image-inner'); node.appendChild(container); return node; } static value(node) { // 블롯의 값을 Delta로 저장할 때 사용 return { url: node.getAttribute('data-url'), width: node.getAttribute('data-width'), height: node.getAttribute('data-height') }; } renderReactComponent() { const node = this.domNode as HTMLElement; const container = node.querySelector('.resizable-image-inner'); const initialWidth = parseInt(node.getAttribute('data-width') || '200', 10); const initialHeight = parseInt(node.getAttribute('data-height') || '200', 10); const url = node.getAttribute('data-url') || ''; // React 18 이상에서는 createRoot 사용 const root = ReactDOM.createRoot(container!); root.render( { // 리사이즈 완료 후 새로운 크기를 data-속성에 저장 node.setAttribute('data-width', String(ref.offsetWidth)); node.setAttribute('data-height', String(ref.offsetHeight)); }} > ); } } Quill.register(ResizableImageBlot); Quill.register('modules/fileUploader', FileUploader); Quill.register(FileBlot); // --------- Quill Editor --------- // 글씨 크기 설정 const FontSizeStyle = Quill.import('attributors/style/size') as any; FontSizeStyle.whitelist = ['11px', '13px', '15px', '16px', '19px', '24px', '28px', '30px', '34px', '38px']; Quill.register(FontSizeStyle, true); interface QuillEditorProps { content: string|null|''; onChange: (content: string) => void; } export default function Editor({ content, onChange }: QuillEditorProps) { const editorRef = useRef(null); const quillInstanceRef = useRef(null); useEffect(() => { if (editorRef.current && !quillInstanceRef.current) { const icons = Quill.import('ui/icons') as Record; // 파일 아이콘 생성 icons['file'] = ' 1126 '; quillInstanceRef.current = new Quill(editorRef.current, { theme: 'snow', modules: { toolbar: { container: [ [{ font: [] }, { size: ['11px', '13px', '15px', '16px', '19px', '24px', '28px', '30px', '34px', '38px']}], ['bold', 'italic', 'underline', 'strike', { color: [] }, { background: [] }], [{ align: [] }, { list: 'ordered' }, { list: 'bullet' }], ['blockquote', 'code-block', 'clean'], ['link', 'image', 'video', 'file'] ], handlers: { file: () => { const uploader = quillInstanceRef.current?.getModule('fileUploader') as FileUploader; uploader?.selectFile(); } }, }, clipboard: { matchVisual: false, }, fileUploader: {} } }); quillInstanceRef.current.root.innerHTML = content || ''; // 에디터 내용 변경 시 quillInstanceRef.current.on('text-change', () => { onChange(quillInstanceRef.current!.root.innerHTML); }); // 첨부파일 클릭 시 활성화 제거 quillInstanceRef.current.on('selection-change', () => { const uploader = quillInstanceRef.current?.getModule('fileUploader') as FileUploader; if (uploader) { uploader.clearActiveCards(); } }); // FileCardBlot에 quillInstanceRef.current를 주입 FileBlot.setQuillInstance(quillInstanceRef.current); const quillRoot = quillInstanceRef.current.root; // drop 위치 처리 quillRoot.addEventListener('dragover', (e) => { e.preventDefault(); }); quillRoot.addEventListener('drop', (e) => { e.preventDefault(); const data = e.dataTransfer?.getData('text/plain'); if (!data) return; const file = JSON.parse(data); // 현재 커서 위치 찾기 const selection = quillInstanceRef.current!.getSelection(); const index = selection ? selection.index : quillInstanceRef.current!.getLength(); // 기존 dragging 된 요소 삭제 const draggingNode = quillRoot.querySelector('.file-card.dragging'); draggingNode?.remove(); // 새로운 위치에 삽입 quillInstanceRef.current!.insertEmbed(index, 'fileCard', file, Quill.sources.USER); quillInstanceRef.current!.setSelection(index + 1); }); } }, []); useEffect(() => { if (quillInstanceRef.current) { const currentHTML = quillInstanceRef.current.root.innerHTML; if (currentHTML !== (content || '')) { quillInstanceRef.current.root.innerHTML = content || ''; } } }, [content]); return (
); }