'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'] = '';
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 (
);
}