|
|
@@ -1,197 +0,0 @@
|
|
|
-'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>
|
|
|
- );
|
|
|
-}
|