'use client'; /** * @author: Kim Jino * @since: 2025.03.31 * @description: CKEditor component for Next.js using Script component for loading CKEditor script */ import '@/styles/editor.scss'; import Script from 'next/script'; import React, { useRef, useState, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react'; import { CKEditor } from '@ckeditor/ckeditor5-react'; import type { Editor as CKEditorInstance } from 'ckeditor5'; import Loading from '@/app/component/Loading'; import BoardMeta from '@/types/forum/boardMeta'; import { PostConst } from '@/constants/forum'; export interface Handle { editorInstance: CKEditorInstance|null; getFileStore(): UploadedFile[]; getImageStore(): UploadedImage[]; getMediaStore(): UploadedMedia[]; } interface Props { editorKey: string; data: string; onChange: (data: string) => void; boardMeta?: BoardMeta|null; } const Editor = forwardRef(({ editorKey = '', data = '', onChange, boardMeta }: Props, ref) => { const [ready, setReady] = useState(false); // CKEditor가 준비되었는지 여부 const [instanceLoaded, setInstanceLoaded] = useState(false); const editorRef = useRef(null); // CKEditor 객체 const editorInstanceRef = useRef(null); useEffect(() => { if (typeof window !== 'undefined' && window.Editor) { editorRef.current = window.Editor; setReady(true); } }, [editorKey]); // 게시판 주소가 바뀌면 다시 ready 확인 const handleScriptLoad = () => { if (typeof window !== 'undefined' && window.Editor) { editorRef.current = window.Editor; setReady(true); } else { console.error('CKEditor script not loaded properly.'); } }; // 외부에 접근 가능한 명령어 useImperativeHandle(ref, () => ({ get editorInstance() { return editorInstanceRef.current; }, getFileStore: () => editorInstanceRef.current?._fileStore || [], getImageStore: () => editorInstanceRef.current?._imageStore || [], getMediaStore: () => editorInstanceRef.current?._mediaStore || [] }), [instanceLoaded]); const editorConfig = useMemo(() => ({ placeholder: '내용을 입력하세요.', maxWordCount: PostConst.MaxAllowedContentLength, // 최대 본문 길이 // 이미지 설정 allowImage: boardMeta?.write.allowImage ?? false, imageUploadLimit: boardMeta?.write.imageUploadLimit, imageUploadMaxSize: boardMeta?.write.imageUploadMaxSize, // 동영상 설정 allowMedia: boardMeta?.write.allowMedia ?? false, mediaUploadLimit: boardMeta?.write.mediaUploadLimit, // 파일 설정 allowFile: boardMeta?.write.allowFile ?? false, fileUploadLimit: boardMeta?.write.fileUploadLimit, fileUploadMaxSize: boardMeta?.write.fileUploadMaxSize, fileUploadExtension: boardMeta?.write.fileUploadExtension /* // 글자수 세기 wordCount: { onUpdate: (stats: { characters: number; words: number }) => { document.getElementById('editorTxtLength')!.innerText = stats.characters.toString(); } } */ }), []); const handleOnReady = useCallback((editor: CKEditorInstance) => { console.log('Editor was initialized'); // 최소 높이 설정 editor.editing.view.change(writer => { const root = editor.editing.view.document.getRoot(); if (root) { writer.setStyle('min-height', '300px', root); } }); editorInstanceRef.current = editor; setInstanceLoaded(true); // 외부 내용을 복사 붙여넣기 가능하도록 처리 editor.editing.view.document.on('paste', (_, data) => { const html = data.dataTransfer.getData('text/html'); const text = data.dataTransfer.getData('text/plain'); const content = (html || text); if (content) { const viewFragment = editor.data.processor.toView(content); const modelFragment = editor.data.toModel(viewFragment); editor.model.change(() => { editor.model.insertContent(modelFragment); }); } }); }, []); const handleChange = useCallback((_: unknown, editor: CKEditorInstance) => { onChange(editor.getData()); // 불필요한 optional chaining 제거 }, [onChange]); return ( <>