| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- '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(
- <Rnd
- default={{
- width: initialWidth,
- height: initialHeight,
- }}
- bounds="parent"
- enableResizing={{
- top: true,
- right: true,
- bottom: true,
- left: true,
- topRight: true,
- bottomRight: true,
- bottomLeft: true,
- topLeft: true,
- }}
- onResizeStop={(e, direction, ref, delta, position) => {
- // 리사이즈 완료 후 새로운 크기를 data-속성에 저장
- node.setAttribute('data-width', String(ref.offsetWidth));
- node.setAttribute('data-height', String(ref.offsetHeight));
- }}
- >
- <img
- src={url}
- alt=""
- style={{ width: '100%', height: '100%', objectFit: 'contain' }}
- />
- </Rnd>
- );
- }
- }
- 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<HTMLDivElement | null>(null);
- const quillInstanceRef = useRef<Quill | null>(null);
- useEffect(() => {
- if (editorRef.current && !quillInstanceRef.current) {
- const icons = Quill.import('ui/icons') as Record<string, string>;
- // 파일 아이콘 생성
- icons['file'] = '<svg viewBox="0 -0.5 17 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="si-glyph si-glyph-file-upload" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <title>1126</title> <defs> </defs> <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g transform="translate(1.000000, 1.000000)" fill="#434343"> <path d="M14,8.047 L14,12.047 L2,12.047 L2,8.047 L0,8.047 L0,15 L15.969,15 L15.969,8.047 L14,8.047 Z" class="si-glyph-fill"> </path> <path d="M7.997,0 L5,3.963 L7.016,3.984 L7.016,8.969 L8.953,8.969 L8.953,3.984 L10.953,3.984 L7.997,0 Z" class="si-glyph-fill"> </path> </g> </g> </g></svg>';
- 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 (
- <div className="w-full">
- <div ref={editorRef} />
- </div>
- );
- }
|