ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [CKEditor5+React] Image & File upload 기능
    ckeditor 2020. 9. 29. 19:29
    반응형

    CKEditor에서 Image, File upload를 어떻게 하는지에 대해 설명 드리겠습니다.

    Official document를 보면 Image & File Upload에 대해 모두 소개한 내용이 있습니다. 

    https://ckeditor.com/docs/ckeditor5/latest/features/image-upload/image-upload.html

     

    Overview - CKEditor 5 Documentation

    Learn how to install, integrate and configure CKEditor 5 Builds and how to work with CKEditor 5 Framework, customize it, create your own plugins and custom editors, change the UI or even bring your own UI to the editor. API reference and examples included.

    ckeditor.com

    글을 보면 upload 기능은 추가 plugin을 설치해야 합니다.

    Image/File upload에 대해서는 하기와 같이 2개 plugin을 추가 구현없이 사용할 수 있습니다.

    a. Easy Image: image를 CKEditor의 cloud에 upload하게 함(우리 서버가 아닌 cloud에 upload하는것임으로 pass함).

    b. CKFinder: image/file 모두 upload가능하고 관리까지 가능한 강력한 plugin. (그런데 "유료"라는 치명적인 단점이 있음...... 그래서 pass 함.)

    그리고 하기 2개 adapter를 제공합니다.

    a. Base64 adapter: image를 base64-encoded string으로 변환하여 저장. (처음에는 file 저장 기능이 요구사항에 없어서 해당 plugin을 사용함. 추가구현 없이, 파일 관리 없이 string을 db에 저장해서 편하기는 했음.)

    b. Simple upload adapter: XMLHttpRequest API를 이용하여 file/image를 server에 upload하고 response를 받아서 처리하는 adapter.

    여기서 plugin은 사용자와의 interface로 생각하면 되고 adapter는 실제 file/image를 server에 send/receive하는 역할을 한다고 보면 됩니다.

    Plugin과 adapter는 ckeditor에서 image/file upload가 어떻게 동작하는지를 보면 더 잘 이해할 수 있습니다.

     

    1. CKEditor5에서의 Image/File upload Architecture

    ckeditor.com/docs/ckeditor5/latest/framework/guides/deep-dive/upload-adapter.html

     

    Custom upload adapter - CKEditor 5 Documentation

    Learn how to install, integrate and configure CKEditor 5 Builds and how to work with CKEditor 5 Framework, customize it, create your own plugins and custom editors, change the UI or even bring your own UI to the editor. API reference and examples included.

    ckeditor.com

    upload architecture는 하기와 같습니다.

    Image/File upload architecture

    • upload adapter를 file repository plugin에 register 함. 
    • 사용자는 Image/File upload plugin을 통하여 upload하고 싶은 Image/File를 선택 함.
    • 선택한 Image/File 정보는 Options으로 Command 객체에 전달 됨.
    • Command 객체에서는 File Repository Plugin을 이용하여 선택한 File 별로 File Loader instance를 생성 함.
    • 생성된 File Loader instance는 disk에 있는 file/image정보를 읽고 1번에서 register된 upload adapter로 Server에 upload 함.
    • Server에서 upload 된 정보를 받아서 editor에 insert 함.

    위의 그림에서의 Image/file upload plugin, Upload adapter만 선택 혹은 구현 해서 editor에 추가하면 upload 기능이 동작합니다(Command는 upload plugin에 포함됬다고 생각하면 됩니다).

     

    2. Image/File upload 기능 구현하기

    Architecture를 이해했으니 Image/File upload 기능을 구현해 보겠습니다.

    adapter는 simple upload adapter를 사용하겠습니다.

    ckeditor.com/docs/ckeditor5/latest/features/image-upload/simple-upload-adapter.html

     

    Simple upload adapter - CKEditor 5 Documentation

    Learn how to install, integrate and configure CKEditor 5 Builds and how to work with CKEditor 5 Framework, customize it, create your own plugins and custom editors, change the UI or even bring your own UI to the editor. API reference and examples included.

    ckeditor.com

    구현하는 기능은 다음과 같습니다.

    • editor toolbar에서 file&image upload icon을 click 함.
    • image를 선택하면 server에 image가 저장되고 editor에 선택한 image가 삽임됨.
    • file를 선택하면 선택된 server에 file이 저장되고 editor에 선택된 file이 삽입됨.
    • 삽입된 file에 mouse를 가져가면 upload한 사용자 이름이 tooltip으로 보여짐(해당 내용은 하기 page에서 소개함).
    • 50mb 이상의 file를 upload하면 upload할 수 없다고 alert가 발생 함.

    먼저, Image upload는 CKEditor5에 이미 기본적으로 지원하는 plugin이 존재합니다.

    @ckeditor/ckeditor5-image/src/imageupload

    해당 plugin의 "imageUpload" 라는 command로 image file를 보내면 simple upload updapter로 image를 server에 보내고 editor에 추가하는 작업을 완료해줍니다. image upload는 해당 plugin을 사용하였습니다.

    koa로 image를 전송받아서 저장하는 간단한 server를 만들었습니다(server 관련해서는 하기 page 참조하시면 되겠습니다.).

    아래에서는 주로 upload_file plugin관련 code를 보여줄거고 blog하단에 source code github 주소를 명시하였습니다.

    (1). File&Image upload plugin 구현

    import _ from "lodash";
    
    import Plugin from "@ckeditor/ckeditor5-core/src/plugin";
    import imageIcon from "@ckeditor/ckeditor5-core/theme/icons/image.svg";
    import FileDialogButtonView from "@ckeditor/ckeditor5-upload/src/ui/filedialogbuttonview";
    import FileRepository from "@ckeditor/ckeditor5-upload/src/filerepository";
    import Notification from "@ckeditor/ckeditor5-ui/src/notification/notification";
    import Command from "@ckeditor/ckeditor5-core/src/command";
    import { findOptimalInsertionPosition } from '@ckeditor/ckeditor5-widget/src/utils';
    
    const _UPLOAD_FILE_LIMIT = 50000000;
    
    const createImageTypeRegExp = (types) => {
      // Sanitize the MIME type name which may include: "+", "-" or ".".
      const regExpSafeNames = types.map((type) => type.replace("+", "\\+"));
    
      return new RegExp(`^image\\/(${regExpSafeNames.join("|")})$`);
    };
    
    class FileUploadCommand extends Command {
      /**
       * Executes the command.
       *
       * @fires execute
       * @param {Object} options Options for the executed command.
       * @param {File|Array.<File>} options.file The image file or an array of image files to upload.
       */
      //fileUpload command를 받으면 execute함수가 실행 됨.
      execute(options) {
        const editor = this.editor;
        const model = editor.model;
    	
        //File Repository 가져옮
        const fileRepository = editor.plugins.get(FileRepository);
        const notification = editor.plugins.get(Notification);
    
        model.change((writer) => {
          const filesToUpload = Array.isArray(options.file)
            ? options.file
            : [options.file];
    
          for (const file of filesToUpload) {
            console.log(file);
            if (file.size > _UPLOAD_FILE_LIMIT) {
              //50mb이상 파일일 때 notification 보냄
              notification.showWarning("Can not upload files larger than 50MB");
              return;
            }
            uploadFile(writer, model, fileRepository, file);
          }
        });
      }
    }
    
    // Handles uploading single file.
    //
    // @param {module:engine/model/writer~writer} writer
    // @param {module:engine/model/model~Model} model
    // @param {File} file
    function uploadFile(writer, model, fileRepository, file) {
      //파일 별 loader instance를 생성 함.
      const loader = fileRepository.createLoader(file);
    
      if (!loader) {
        return;
      }
    
      //loader가 file를 disk에서 read() 후 upload()로 server에 전송 함.
      loader
        .read()
        .then(() => loader.upload())
        .then((data) => {
          const attributes = {
            linkHref: data.default,
            titleTarget: data.editor ? data.editor : ""
          }
    
          console.log(data.default);
    
    	  //server에 return된 정보로 <a> tag로 되어 있는 file element를 생성 함.
          const fileElement = writer.createText(file.name, attributes);
    
          const insertAtSelection = findOptimalInsertionPosition(
            model.document.selection,
            model
          );
    	  
          //editor에 <a> tag로 되어 있는 file element를 삽입 함.
          model.insertContent(fileElement, insertAtSelection);
        });
    }
    
    class Uploader extends Plugin {
      init() {
        const editor = this.editor;
        //fileUpload command에 위에서 구현한 FileUploadCommand를 연동 시킴
        editor.commands.add("fileUpload", new FileUploadCommand(editor));
    
        editor.ui.componentFactory.add("insertFileAndImage", (locale) => {
          const view = new FileDialogButtonView(locale);
          const imageTypes = editor.config.get("image.upload.types");
          const imageTypesRegExp = createImageTypeRegExp(imageTypes);
    
          view.buttonView.set({
            label: "Insert image and file",
            icon: imageIcon,
            tooltip: true,
          });
    
    	  //사용자가 upload할 file/image를 선택 시 done event가 발생함.
          view.on("done", (evt, files) => {
            const [imagesToUpload, filesToUpload] = _.partition(files, (file) =>
              imageTypesRegExp.test(file.type)
            );
    
            if (imagesToUpload.length) {
              editor.execute("imageUpload", { file: imagesToUpload });
            }
    
            if (filesToUpload.length) {
              editor.execute("fileUpload", { file: filesToUpload });
            }
          });
    
          return view;
        });
      }
    }
    
    export default Uploader;

     

    코드가 위의 Image Upload Architecture 에서 보여준것 처럼 구현됬음을 알 수 있습니다(Adapter는 CKEditor에서 제공하는 Simple Upload Adapter를 사용하기 때문에 adapter regist 하는 부분은 없습니다).

    (2). 구현한 Upload plugin을 editor에 추가 함.

    import React from "react";
    import CKEditor from "@ckeditor/ckeditor5-react";
    import DecoupledEditor from "@ckeditor/ckeditor5-editor-decoupled/src/decouplededitor";
    import Essentials from "@ckeditor/ckeditor5-essentials/src/essentials";
    import Paragraph from "@ckeditor/ckeditor5-paragraph/src/paragraph";
    import Bold from "@ckeditor/ckeditor5-basic-styles/src/bold";
    import Italic from "@ckeditor/ckeditor5-basic-styles/src/italic";
    import Underline from "@ckeditor/ckeditor5-basic-styles/src/underline";
    import Strikethrough from "@ckeditor/ckeditor5-basic-styles/src/strikethrough";
    import BlockQuote from "@ckeditor/ckeditor5-block-quote/src/blockquote";
    import Link from "@ckeditor/ckeditor5-link/src/link";
    import PasteFromOffice from "@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice";
    import Heading from "@ckeditor/ckeditor5-heading/src/heading";
    import Font from "@ckeditor/ckeditor5-font/src/font";
    import Image from "@ckeditor/ckeditor5-image/src/image";
    import ImageStyle from "@ckeditor/ckeditor5-image/src/imagestyle";
    import ImageToolbar from "@ckeditor/ckeditor5-image/src/imagetoolbar";
    import ImageUpload from "@ckeditor/ckeditor5-image/src/imageupload";
    import ImageResize from "@ckeditor/ckeditor5-image/src/imageresize";
    import List from "@ckeditor/ckeditor5-list/src/list";
    import Alignment from "@ckeditor/ckeditor5-alignment/src/alignment";
    import Table from "@ckeditor/ckeditor5-table/src/table";
    import TableToolbar from "@ckeditor/ckeditor5-table/src/tabletoolbar";
    import TextTransformation from "@ckeditor/ckeditor5-typing/src/texttransformation";
    import Indent from "@ckeditor/ckeditor5-indent/src/indent";
    import IndentBlock from "@ckeditor/ckeditor5-indent/src/indentblock";
    import TableProperties from "@ckeditor/ckeditor5-table/src/tableproperties";
    import TableCellProperties from "@ckeditor/ckeditor5-table/src/tablecellproperties";
    import SimpleUploadAdapter from "@ckeditor/ckeditor5-upload/src/adapters/simpleuploadadapter";
    import Uploader from "./Uploader";
    import LinkTitle from "./LinkTitle";
    
    const Editor = (props) => {
      return (
        <div>
          <CKEditor
            onInit={(editor) => {
              editor.ui
                .getEditableElement()
                .parentElement.insertBefore(
                  editor.ui.view.toolbar.element,
                  editor.ui.getEditableElement()
                );
            }}
            config={{
              language: "ko",
              plugins: [
                Essentials,
                Paragraph,
                Bold,
                Italic,
                Heading,
                Indent,
                IndentBlock,
                Underline,
                Strikethrough,
                BlockQuote,
                Font,
                Alignment,
                List,
                Link,
                PasteFromOffice,
                Image,
                ImageStyle,
                ImageToolbar,
                ImageResize,
                Table,
                TableToolbar,
                TableProperties,
                TableCellProperties,
                TextTransformation,
                //구현한 Uploader plugin, ImageUpload plugin, Simple Upload Adpter 추가
                Uploader,
                ImageUpload,
                SimpleUploadAdapter,
                LinkTitle
              ],
              toolbar: props.toolbar
                ? props.toolbar
                : [
                    "heading",
                    "|",
                    "bold",
                    "italic",
                    "underline",
                    "strikethrough",
                    "|",
                    "fontSize",
                    "fontColor",
                    "fontBackgroundColor",
                    "|",
                    "alignment",
                    "outdent",
                    "indent",
                    "bulletedList",
                    "numberedList",
                    "blockQuote",
                    "|",
                    "link",
                    "insertTable",
                    //toolbar에 위에서 구현한 plugin 버튼 추가
                    "insertFileAndImage",
                    "|",
                    "undo",
                    "redo",
                  ],
              fontSize: {
                options: [
                  14,
                  15,
                  16,
                  17,
                  18,
                  19,
                  'default',
                  21,
                  22,
                  23,
                  24,
                  25,
                  26,
                  27,
                  28,
                  29,
                  30,
                ],
              },
              alignment: {
                options: ["justify", "left", "center", "right"],
              },
              table: {
                contentToolbar: [
                  "tableColumn",
                  "tableRow",
                  "mergeTableCells",
                  "tableProperties",
                  "tableCellProperties",
                ],
              },
              image: {
                resizeUnit: "px",
                toolbar: [
                  "imageStyle:alignLeft",
                  "imageStyle:full",
                  "imageStyle:alignRight",
                  "|",
                  "imageTextAlternative",
                ],
                styles: ["full", "alignLeft", "alignRight"],
                type: ["JPEG", "JPG", "GIF", "PNG"],
              },
              typing: {
                transformations: {
                  remove: [
                    "enDash",
                    "emDash",
                    "oneHalf",
                    "oneThird",
                    "twoThirds",
                    "oneForth",
                    "threeQuarters",
                  ],
                },
              },
              //Simple Upload Adapter 설정
              simpleUpload: {
                uploadUrl: "/uploadFile",
                withCredentials: true,
                headers: {
                  "upload-folder": props.uploadFolder ? props.uploadFolder : "root",
                  "upload-editor": props.uploader ? props.uploader : "",
                },
              },
            }}
            editor={DecoupledEditor}
            {...props}
          />
        </div>
      );
    };
    
    export default Editor;
    

     

    전체 source code는 하기 github에 있습니다(client & server 코드가 다 포함되어 있습니다).

    source code: github.com/quanjichun/ckeditor-react-integrate

     

     

    반응형

    댓글

Designed by Tistory.