에디터를 결정하기에 앞서 여러가지를 테스트해봤다.

- slate: 데이터가 json 형태라 기존 데이터와 호환성 문제로 fail

- react-draft: 데이터가 객체형태라 viewer로 따로 써야하고 어쩌고 하는데 무엇보다 반응속도라 느려서 fail

- quill: toolbar를 세부항목까지 자유롭게 커스텀이 가능한 점은 좋았으나 표나 이미지 업로드 기능같은걸 내가 구현해야한다는 점에서 내가 게을러서 fail

- jodit: 무료는 로고 존재감도 쎄고 문서가 불친절해서 보류했다가 tiny mce로 결정해서 fail

- tiny mce: 문서가 매우 잘 되어있고 커뮤니티 버전으로도 어지간한 기능은 다 있어서 최종 결정하게 되었다.

 

import dynamic from 'next/dynamic'
import React, { useState, useRef, useMemo, useEffect, useCallback } from 'react'

const Editor = dynamic(() => import('@tinymce/tinymce-react').then((mode) => mode.Editor), {
	ssr: false,
})

// plugin 기능 import 역할
const tinymcePlugins = [
	'link',
	'lists', // 기본 ol, ul
	'image',
	'autoresize',
	'autolink',
	'advlist', // ol, ul 불릿이 좀 더 여러가지가 생김
	'table',
	'preview',
	'searchreplace', // 치환기능
	'visualblocks', // 태그를 실루엣으로 보여주어 태그가 꼬일경우 편집하는데 도움됨
	'code', // 코드삽입기능ㄴㄴ html 태그로 보여주는 기능임
	'fullscreen',
	'media', // 동영상
	'accordion', // 티스토리 기준 접은글 기능
	'autosave', // localstrage에 저장함
]

// toolbar에 해당 기능을 나열함. plugin이 필요한 기능의 경우 위에도 아래에도 둘 다 들어가야함
const tinymceToolbar =
	'undo redo | blocks fontsize |' +
	'bold italic underline strikethrough forecolor backcolor |' +
	'bullist numlist blockquote|' +
	'alignleft aligncenter alignright alignjustify | outdent indent |' +
	'link image table accordion media | removeformat visualblocks searchreplace restoredraft | fullscreen preview code'

// 온갖 설정은 여기에 다 넣는다고 보면 됨
const editorInitOption = {
    language: 'ko_KR', // 한국어팩
    plugins: tinymcePlugins,
    toolbar: tinymceToolbar,
    color_default_background: '#E67E23', // fontBackColor 기본 선택은 되지만 "@tinymce/tinymce-react": "^4.3.0" 기준 pallete에서 선택은 안되어 있다.
	color_default_foreground: window.matchMedia('(prefers-color-scheme: dark)').matches ? '#fff' : '#000', // fontColor 기본 선택
    
    // dark skin 적용
    // toolbar css 변경 방법은 찾고 있다.
    skin: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'oxide-dark' : 'oxide',
    // dark 대신 css 파일 경로를 넣어줄수도 있다.
    // dark 적용후 import 되는 css 파일을 받아서 dark 기본색인 #222f3를 수정후 사용하면 좋다.
    content_css: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default',
    
    autosave_interval: '20s',
    autosave_restore_when_empty: true, // 빈 편집기에 마지막 저장데이터 복원여부 확인
    autosave_retention: '30m', // 30분 넘은 저장데이터는 삭제
    autosave_prefix: 'tinymce-autosave-{path}{query}-{id}-',
    // min_height: 250,
    // max_height: document.querySelector('#__next').offsetHeight - 250,
    menubar: false,
    branding: false,
    statusbar: false,
    
    // automatic_uploads: false, // true가 기본값
    // 이미지를 업로드할 서버 api 주소. 
    // 다음과 같은 형태로 반환해야 정상적으로 동작한다. 
    // { "location": "folder/sub-folder/new-location.png" }
    images_upload_url: '/api/image-uploads',  
    images_upload_base_path: '/api/files', // 업로드한 파일의 중복경로
    images_upload_credentials: true, // 인증정보가 cookie면 true
    file_picker_callback: (callback, value, meta) => {	
        const input = document.createElement('input')
        input.setAttribute('accept', 'image/*')
        input.setAttribute('type', 'file')
        input.onchange = function () {
            const file = this.files[0]
            const reader = new FileReader()
            reader.onload = function () {
                const id = 'blobid' + new Date().getTime()
                const blobCache = tinymce.activeEditor.editorUpload.blobCache
                const base64 = reader.result.split(',')[1]
                const blobInfo = blobCache.create(id, file, base64)
                blobCache.add(blobInfo)
                callback(blobInfo.blobUri(), { alt: file.name })
            }
            reader.readAsDataURL(file)
        }
        input.click()
    },
    
    // 이미지 업로드 중 커스텀 기능을 넣어야한다면 이걸로 처리해야 함
    // 위에서 설정한 항목중 images_upload_url, images_upload_base_path, images_upload_credentials를 쓰지 않고 내부 코드을 사용함
    // convert_urls: false, // 아래에서 resolve가 절대경로를 반환하는 거라면 false 기본값은 true임
    // images_upload_handler: (blobInfo, progress) => 
    //   
    // 	new Promise((resolve, reject) => {
    //		const images_upload_url = '/api/image-uploads' // 이미지 업로드 api 주소
    // 		const xhr = new XMLHttpRequest()
    // 		xhr.withCredentials = true // 인증정보가 cookie면 true
    // 		xhr.open('POST', images_upload_url)

    // 		xhr.upload.onprogress = (e) => {
    // 			progress((e.loaded / e.total) * 100)
    // 		}

    // 		xhr.onload = () => {
    // 			if (xhr.status === 403) {
    // 				reject({ message: 'HTTP Error: ' + xhr.status, remove: true })
    // 				return
    // 			}
    // 			if (xhr.status < 200 || xhr.status >= 300) {
    // 				reject('HTTP Error: ' + xhr.status)
    // 				return
    // 			}

    // 			const json = JSON.parse(xhr.responseText)
    // 			if (!json || typeof json.location != 'string') {
    // 				reject('Invalid JSON: ' + xhr.responseText)
    // 				return
    // 			}
    
    //			/* 커스텀 설정 넣을 곳 */

    // 			resolve(json.location) // 경로
    // 		}

    // 		xhr.onerror = () => {
    // 			reject('Image upload failed due to a XHR Transport error. Code: ' + xhr.status)
    // 		}

    // 		const formData = new FormData()
    // 		formData.append('file', blobInfo.blob(), blobInfo.filename())

    // 		xhr.send(formData)
    // 	}),
}

export default function Editor() {
	const editorRef = useRef(null)
    
    // editorRef.current.getContent()
    
	return <Editor
        onInit={(e, editor) => (editorRef.current = editor)}
        apiKey="API키"
        initialValue={에디터에 출력할 기본값이 있다면 여기}
        init={editorInitOption}
    />
}

toolbar css는 일단.... _doucument 파일에다 <NextScript /> 아래에 <link rel="stylesheet" href="" /> 태그를 삽입하는 방식으로 일단 처리하려고 한다. 대신에 @media (prefers-color-scheme: dark) { } 로 감싸주었다.

+ Recent posts