Editor.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. 'use client';
  2. /**
  3. * @author: Kim Jino
  4. * @since: 2025.03.31
  5. * @description: CKEditor component for Next.js using Script component for loading CKEditor script
  6. */
  7. // @/app/(forum)/post/_component/Editor.tsx
  8. import '@/styles/editor.scss';
  9. import Script from 'next/script';
  10. import React, { useRef, useState, useEffect, forwardRef, useImperativeHandle } from 'react';
  11. import { CKEditor } from '@ckeditor/ckeditor5-react';
  12. import type { Editor as CKEditorInstance } from '@ckeditor/ckeditor5-core';
  13. import Loading from '@/app/component/Loading';
  14. import BoardMeta from '@/types/forum/boardMeta';
  15. import { PostConst } from '@/constants/forum';
  16. export interface Handle {
  17. editorInstance: CKEditorInstance|null;
  18. getFileStore(): UploadedFile[];
  19. getImageStore(): UploadedImage[];
  20. getMediaStore(): UploadedMedia[];
  21. }
  22. interface Props {
  23. key: string;
  24. data: string;
  25. onChange: (data: string) => void;
  26. boardMeta?: BoardMeta|null;
  27. }
  28. const Editor = forwardRef<Handle, Props>(({ key = '', data = '', onChange, boardMeta }: Props, ref) =>
  29. {
  30. const [ready, setReady] = useState<boolean>(false); // CKEditor가 준비되었는지 여부
  31. const [instanceLoaded, setInstanceLoaded] = useState<boolean>(false);
  32. const editorRef = useRef<typeof window.Editor|null>(null); // CKEditor 객체
  33. const editorInstanceRef = useRef<CKEditorInstance|null>(null);
  34. useEffect(() => {
  35. if (typeof window !== 'undefined' && window.Editor) {
  36. editorRef.current = window.Editor;
  37. setReady(true);
  38. }
  39. }, [key]); // 게시판 주소가 바뀌면 다시 ready 확인
  40. const handleScriptLoad = () => {
  41. if (typeof window !== 'undefined' && window.Editor) {
  42. editorRef.current = window.Editor;
  43. setReady(true);
  44. } else {
  45. console.error('CKEditor script not loaded properly.');
  46. }
  47. };
  48. // 외부에 접근 가능한 명령어
  49. useImperativeHandle(ref, () => ({
  50. editorInstance: editorInstanceRef.current,
  51. getFileStore: () => editorInstanceRef.current?._fileStore || [],
  52. getImageStore: () => editorInstanceRef.current?._imageStore || [],
  53. getMediaStore: () => editorInstanceRef.current?._mediaStore || []
  54. }), [instanceLoaded]);
  55. return (
  56. <>
  57. <Script src="/editor/editor.min.js" strategy="afterInteractive" onLoad={handleScriptLoad}/>
  58. {ready && editorRef.current ? (
  59. <CKEditor
  60. editor={editorRef.current}
  61. data={data}
  62. config={{
  63. placeholder: '내용을 입력하세요.',
  64. maxWordCount: PostConst.maxAllowedContentLength, // 최대 본문 길이
  65. // 이미지 설정
  66. allowImage: boardMeta?.write.allowImage ?? false,
  67. imageUploadLimit: boardMeta?.write.imageUploadLimit,
  68. imageUploadMaxSize: boardMeta?.write.imageUploadMaxSize,
  69. // 동영상 설정
  70. allowMedia: boardMeta?.write.allowMedia ?? false,
  71. mediaUploadLimit: boardMeta?.write.mediaUploadLimit,
  72. // 파일 설정
  73. allowFile: boardMeta?.write.allowFile ?? false,
  74. fileUploadLimit: boardMeta?.write.fileUploadLimit,
  75. fileUploadMaxSize: boardMeta?.write.fileUploadMaxSize,
  76. fileUploadExtension: boardMeta?.write.fileUploadExtension
  77. /*
  78. // 글자수 세기
  79. wordCount: {
  80. onUpdate: (stats: { characters: number; words: number }) => {
  81. document.getElementById('editorTxtLength')!.innerText = stats.characters.toString();
  82. }
  83. }
  84. */
  85. }}
  86. onReady={(editor) => {
  87. console.log('Editor was initialized');
  88. // 최소 높이 설정
  89. editor.editing.view.change(writer => {
  90. const root = editor.editing.view.document.getRoot();
  91. if (root) {
  92. writer.setStyle('min-height', '300px', root);
  93. }
  94. });
  95. editorInstanceRef.current = editor;
  96. setInstanceLoaded(true);
  97. // 외부 내용을 복사 붙여넣기 가능하도록 처리
  98. editor.editing.view.document.on('paste', (_, data) => {
  99. const html = data.dataTransfer.getData('text/html');
  100. const text = data.dataTransfer.getData('text/plain');
  101. const content = (html || text);
  102. if (content) {
  103. const viewFragment = editor.data.processor.toView(content);
  104. const modelFragment = editor.data.toModel(viewFragment);
  105. editor.model.change(() => {
  106. editor.model.insertContent(modelFragment);
  107. });
  108. }
  109. });
  110. }}
  111. onChange={(_, editor) => {
  112. onChange?.(editor.getData());
  113. }}
  114. />
  115. ) : (
  116. <Loading/>
  117. )}
  118. </>
  119. );
  120. });
  121. // 컴포넌트 이름 지정
  122. Editor.displayName = 'Editor';
  123. export default Editor;