나의 이야기

프론트/vue

vue3에서 ckeditor 사용방법(바인딩, 이미지 업로드 포함)

 

게시글을 작성시에 에디터는 필수로 들어가는 요소이다

vue3와 어울리는 쓸만한 editor를 찾다 커스텀도 자유로이 가능한 ckeditor를 발견했다

아래는 내가 사용한 ckeditor사용방법이다.

1. ckeditor 패키지 설치

npm install --save @ckeditor/ckeditor5-vue @ckeditor/ckeditor5-build-classic

 

2. ckeditor 설정

import { createApp } from "vue";
import { createPinia } from "pinia";
import router from "@/routes";
import CKEditor from '@ckeditor/ckeditor5-vue'
import "@/assets/css/editor/content-style.css";

let app = createApp(App);  
let pinia = createPinia();  

app.config.globalProperties.$axios = axios;
app.use(router);
app.use(pinia);
app.use(CKEditor);

app.mount("#app");

 

3. ckeditor 사용

- 기존에 content가 존재할 시 props로 content를 받는다

- props데이터를 바인딩할 수 있도록 v-model에 text를 바인딩 한다( 6번의 watch 함수에서 기존 데이터 text변수에 주입 )

<template>
  <Ckeditor
    :editor="editor"
    v-model="text"
    :config="editorConfig"
    @ready="onEditorReady"
  ></Ckeditor>
</template>

<script setup>
import { ClassicEditor } from "@ckeditor/ckeditor5-editor-classic";
import { Essentials } from "@ckeditor/ckeditor5-essentials";
import { Bold, Italic } from "@ckeditor/ckeditor5-basic-styles";
import { BlockQuote } from "@ckeditor/ckeditor5-block-quote";
import { Font } from "@ckeditor/ckeditor5-font";
import { Link } from "@ckeditor/ckeditor5-link";
import { Paragraph } from "@ckeditor/ckeditor5-paragraph";
import { Indent } from "@ckeditor/ckeditor5-indent";
import { List } from "@ckeditor/ckeditor5-list";
import { Heading } from "@ckeditor/ckeditor5-heading";
import {
  Table,
  TableColumnResize,
  TableToolbar,
} from "@ckeditor/ckeditor5-table";
import { TextTransformation } from "@ckeditor/ckeditor5-typing";
import { Alignment } from "@ckeditor/ckeditor5-alignment";
import {
  Image,
  ImageCaption,
  ImageStyle,
  ImageToolbar,
  ImageUpload,
  ImageResize,
} from "@ckeditor/ckeditor5-image";

import { reactive, ref, watch } from "vue";
import UploadAdapter from "#/Editor/UploadAdapter";
import axios from "@/utils/axiosInstance.js";

let text = ref();

const props = defineProps({
  selectedContent: String,
  boardIdx: Number,
  isContentUpdating: Boolean,
});

const emit = defineEmits([
  "update:modelValue",
  "update:images",
  "update:deletedImages",
]);

const editor = ClassicEditor;
const editorConfig = {
  extraPlugins: [CustomUploadAdapterPlugin],
  plugins: [
    Essentials,
    Heading,
    Bold,
    Italic,
    Font,
    Link,
    Paragraph,
    BlockQuote,
    Indent,
    List,
    Table,
    TableToolbar,
    TableColumnResize,
    TextTransformation,
    Alignment,
    Image,
    ImageCaption,
    ImageStyle,
    ImageToolbar,
    ImageUpload,
    ImageResize,
  ],
  toolbar: {
    items: [
      "heading",
      "bold",
      "italic",
      "fontColor",
      "fontBackgroundColor",
      "link",
      "imageUpload",
      "indent",
      "outdent",
      "numberedList",
      "alignment",
      "blockQuote",
      "undo",
      "redo",
      "insertTable",
    ],
  },
  image: {
    toolbar: [
      "toggleImageCaption",
      "imageStyle:inline",
      "imageStyle:block",
      "imageStyle:side",
    ],
  },
  table: {
    contentToolbar: [
      "tableColumn",
      "tableRow",
      "mergeTableCells",
      "tableProperties",
      "tableCellProperties",
    ],
  },
  language: "ko",
};

</script>

<style>
@import "@ckeditor/ckeditor5-editor-classic/theme/classiceditor.css";

.ck.ck-editor {
  font-size: 14px;
}

.ck-editor__editable {
  min-height: 300px;
  max-height: 350px;
}
.ck-content {
  /* padding-left: 24px !important; */
}
ol {
  padding-left: 15px !important;
}
strong {
  font-weight: bold !important;
}
i {
  font-style: italic !important;
}
h2 {
  font-size: 2em !important;
}
h3 {
  font-size: 1.5em !important;
}
h4 {
  font-size: 1.17em !important;
}
</style>

 

4. 이미지 UploadAdapter.js 구현

// util/axiosInstance.js
import axios from 'axios';

const instance = axios.create({
  baseURL: 'http://localhost:8080/api/'
});

// UploadAdapter.js
import axios from '@/utils/axiosInstance.js';

export default class UploadAdapter {
    constructor(loader, uplodedImageUrls) {
        this.loader = loader;
        this.uplodedImageUrls = uplodedImageUrls;
        this.loader.file.then((pic) => (this.file = pic));
    }

    // Starts the upload process.
    upload() {
        return this.loader.file.then((uploadedFile) => {
            return new Promise((resolve, reject) => {
                const formData = new FormData();
                formData.append('upload', uploadedFile);

                axios.post('/editor/imgUpload', formData, {
                    headers: {
                        'Content-Type': 'multipart/form-data'
                    }
                })
                .then((res) => {
                    const returnUrl = res.data.url;
                    const decodeUrl = decodeURIComponent(returnUrl);
                    // console.log('File uploaded:', returnUrl, decodeUrl); // 업로드된 URL을 로그로 확인
                    this.uplodedImageUrls.push(decodeUrl); 
                    console.log('Image URL added to uplodedImageUrls:', this.uplodedImageUrls); // 추가된 URL을 로그로 확인
                    resolve({
                         default: decodeUrl,
                    });
                })
                .catch((error) => {
                    console.error('File upload failed:', error);
                    reject(error.response?.data?.message || 'Upload failed');
                });
            });
        });
    }
}

 

5. image처리 로직 추가

const editor = ClassicEditor;
let imageUrls = reactive([]); // 이미지 URL들을 저장할 배열
let deletedImageUrls = reactive([]); // 이미지 URL들을 저장할 배열

// Custom Upload Adapter Plugin function
function CustomUploadAdapterPlugin(editor) {
  editor.plugins.get("FileRepository").createUploadAdapter = (loader) => {
    return new UploadAdapter(loader, imageUrls);
  };
}

const deleteImageFromServer = async (imageURL) => {
  const imageName = imageURL.split("/").pop();
  return await axios
    .delete(`/editor/imgDelete/${imageName}`)
    .then((response) => {
      console.log("Image deleted successfully");
    })
    .catch((error) => {
      console.error("Error deleting image:", error);
    });
};

const onEditorReady = (editorInstance) => {
  editorInstance.model.document.on("change:data", () => {
    if (props.isContentUpdating) return;

    const editorContent = editorInstance.getData();

    for (let i = 0; i < imageUrls.length; i++) {
      imageUrls[i] = decodeURIComponent(imageUrls[i]);
      if (!editorContent.includes(imageUrls[i])) {
        deleteImageFromServer(imageUrls[i]);
        let deletedImageName = imageUrls[i].replace(
          "http://localhost:8080",
          ""
        );
        deletedImageUrls.push(deletedImageName); // 삭제된 이미지를 추가
        imageUrls.splice(i, 1);
      }
    }
  });
};

// 모든 이미지를 content에서 추출하여 imageUrls에 넣는다
const extractInitialImageUrls = (content) => {
  const parser = new DOMParser();
  const doc = parser.parseFromString(content, "text/html");
  const imgs = doc.querySelectorAll("img");
  imageUrls.length = 0; // 기존 이미지 URL 목록을 초기화합니다.
  imgs.forEach((img) => {
    imageUrls.push(img.src);
  });
};

watch(
  () => props.selectedContent,
  () => {
    text.value = props.selectedContent;
    extractInitialImageUrls(props.selectedContent);
  },
  { immediate: true }
);

 

백엔드 이미지 처리 SpringBoot

2024.07.23 - [스프링] - ckeditor의 이미지 업로드 및 삭제 백엔드( SpringBoot )

'프론트 > vue' 카테고리의 다른 글

vue Multi-word오류(eslint) 해결책  (0) 2023.03.30
Router-view 사용하기  (0) 2023.03.30
vue3 프로젝트 생성  (0) 2023.03.30