React

 

 

참고: https://ccomccomhan.tistory.com/284

 

다음 링크에서 원하는 설정을 통해 ckeditor 를 설정한다.

https://ckeditor.com/ckeditor-5/builder/

 

CKEditor 5는 두 가지 주요 라이선스로 제공됩니다: 오픈 소스 라이선스 (GPL, LGPL, MPL)와 상업적 라이선스. 상업적 라이선스의 경우, 특정 기능을 포함하거나 상업적인 목적(예: 쇼핑몰)으로 사용하는 경우 비용을 지불해야 할 수 있습니다.

오픈 소스 라이선스

CKEditor 5의 기본 기능은 오픈 소스 라이선스(GPL, LGPL, MPL)로 제공되며, 이를 사용하여 무료로 편집기를 설정하고 사용할 수 있습니다. 그러나 이 라이선스는 다음과 같은 제한이 있습니다:

  1. 소스 코드 공개: 오픈 소스 라이선스 조건에 따라, CKEditor를 포함한 프로젝트의 소스 코드를 공개해야 합니다.
  2. 상업적 사용 제한: GPL 라이선스는 상업적 목적으로 사용하기에 적합하지 않을 수 있습니다.

상업적 라이선스

상업적 라이선스를 사용하면 다음과 같은 장점이 있습니다:

  1. 소스 코드 비공개: 상업적 라이선스를 사용하면 프로젝트의 소스 코드를 공개할 필요가 없습니다.
  2. 추가 기능 사용: CKEditor의 추가 플러그인과 상업적 기능을 사용할 수 있습니다.
  3. 상업적 사용: 상업적 목적(예: 쇼핑몰)으로 자유롭게 사용할 수 있습니다.

쇼핑몰에서 사용 가능 여부

쇼핑몰과 같은 상업적 프로젝트에서 CKEditor 5를 사용하려면 상업적 라이선스를 구입하는 것이 좋습니다. 이를 통해 법적 문제를 피하고, 추가 기능을 활용하며, 소스 코드를 공개하지 않아도 됩니다.

라이선스 확인 및 구매

CKEditor 5 라이선스에 대한 자세한 정보와 구매는 CKEditor의 공식 웹사이트에서 확인할 수 있습니다. 상업적 라이선스가 필요한 경우, 해당 페이지에서 라이선스 옵션을 검토하고 구매할 수 있습니다.

결론

쇼핑몰에서 CKEditor 5를 사용하려면 상업적 라이선스를 구입하는 것이 좋습니다. 이렇게 하면 법적 문제를 피하고, 더 많은 기능을 활용하며, 소스 코드를 공개하지 않아도 됩니다. 라이선스 옵션과 가격은 CKEditor의 공식 웹사이트를 통해 확인하시기 바랍니다.

 

 

 

 

프리미엄 기능을 사용하지 않는 CKEditor 5의 경우,

오픈 소스 라이선스(GPL, LGPL, MPL)로 제공됩니다. 이 라이선스는 소스 코드 공개와 관련된 요구 사항이 있습니다. 이를 더 구체적으로 살펴보면:

  1. GPL (GNU General Public License):

    • 소스 코드를 비공개로 사용할 수 없습니다.
    • 소프트웨어를 배포할 때 소스 코드도 함께 제공해야 합니다.
    • 수정된 소프트웨어를 배포할 경우, 수정된 소스 코드도 공개해야 합니다.
  2. LGPL (GNU Lesser General Public License):

    • 소스 코드를 비공개로 사용할 수 있습니다.
    • LGPL로 라이선스된 라이브러리를 사용하여 만든 소프트웨어를 상업적으로 배포할 수 있지만, 라이브러리 자체를 수정하여 배포할 경우 수정된 부분의 소스 코드를 공개해야 합니다.
  3. MPL (Mozilla Public License):

    • MPL로 라이선스된 파일의 수정본을 배포할 때는 해당 파일의 소스 코드를 공개해야 합니다.
    • 프로젝트 전체의 소스 코드를 공개할 필요는 없습니다. MPL로 라이선스된 파일만 공개하면 됩니다.

상업적 사용 및 소스 코드 공개

  • 프로젝트 소스 코드 공개 여부: LGPL 또는 MPL을 사용할 경우, 소스 코드 공개 의무는 제한적입니다. LGPL을 사용하면 CKEditor 자체를 수정하지 않는 한 소스 코드를 공개하지 않아도 됩니다. MPL을 사용하면 MPL로 라이선스된 파일만 공개하면 됩니다.
  • 상업적 사용: CKEditor 5의 오픈 소스 라이선스는 상업적 사용을 허용합니다. 그러나 GPL 라이선스를 사용할 경우, 소스 코드 공개가 필요한 상황이 발생할 수 있습니다. LGPL과 MPL은 상업적 사용에 더 적합합니다.

결론

프리미엄 기능을 사용하지 않는 CKEditor 5의 경우, 프로젝트 소스 코드를 공개해야 하는지 여부는 사용된 오픈 소스 라이선스에 따라 다릅니다. LGPL 또는 MPL 라이선스를 사용하면 소스 코드 공개 의무가 제한적이므로, 상업적 프로젝트에서 더 유리할 수 있습니다.

프로젝트에서 오픈 소스 라이선스를 준수하기 어려운 경우, 상업적 라이선스를 고려하는 것도 좋은 방법입니다. 이를 통해 소스 코드 공개 의무를 피하고 법적 문제를 방지할 수 있습니다.

 

 

 

프리미엄 기능을 사용하지 않고 오픈 소스 버전의 CKEditor 5를 사용하는 경우,

사용된 라이선스에 따라 소스 코드 공개 의무가 다릅니다.

  • GPL: 소스 코드를 공개해야 합니다.
  • LGPL: CKEditor 5 자체를 수정하지 않는 한, 소스 코드를 공개하지 않아도 됩니다.
  • MPL: MPL로 배포된 파일만 공개하면 되므로, 프로젝트 전체의 소스 코드를 공개할 필요는 없습니다.

 

 

 

 

 

다음은 아래는 

 Premium  이 아닌 ckeditor 5 플러그인 들만 조합해서 적용해 보았다.

: CKEditor 5 자체를 수정한것이 아니기 때문에  프로젝트 소스 코드를 공개하지 않아도 될것 같다.

그러나, 자세한 사항은 

프로젝트에서 오픈 소스 라이선스를 준수하기 어려운 경우, 상업적 라이선스를 고려하는 것도 좋은 방법입니다. 이를 통해 소스 코드 공개 의무를 피하고 법적 문제를 방지할 수 있습니다

 

Premium 을 선택할 경우 라이센스 가 필요하다.

 

 

 


 

 

다음은 Page Router 방식의 Next.js 14 에 적용한  코드 이다.

 

 

 

1.리액트 설치

npm install ckeditor5
npm install @ckeditor/ckeditor5-react

 

 

2. style 설정

ckeditor.css

@import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400;1,700&display=swap');

@import url('https://fonts.googleapis.com/css2?family=Black+Han+Sans&family=Dongle&family=Nanum+Gothic&family=Nanum+Gothic+Coding&family=Nanum+Myeongjo&family=Nanum+Pen+Script&family=Noto+Sans+KR:wght@100..900&family=Noto+Serif+KR:wght@200..900&family=Song+Myung&display=swap');

@media print {
	body {
		margin: 0 !important;
	}
}

.main-container {
	--ckeditor5-preview-height: 700px;
	font-family: 'Noto Sans KR, sans-serif';
	width: fit-content;
	margin-left: auto;
	margin-right: auto;
}

.ck-content {
	font-family: 'Lato';
	line-height: 1.6;
	word-break: break-word;
}

.editor-container__editor-wrapper {
	display: flex;
	width: fit-content;
}

.editor-container_document-editor {
	border: 1px solid var(--ck-color-base-border);
}

.editor-container_document-editor .editor-container__toolbar {
	display: flex;
	position: relative;
	box-shadow: 0 2px 3px hsla(0, 0%, 0%, 0.078);
}

.editor-container_document-editor .editor-container__toolbar > .ck.ck-toolbar {
	flex-grow: 1;
	width: 0;
	border-bottom-right-radius: 0;
	border-bottom-left-radius: 0;
	border-top: 0;
	border-left: 0;
	border-right: 0;
}

.editor-container_document-editor .editor-container__menu-bar > .ck.ck-menu-bar {
	border-bottom-right-radius: 0;
	border-bottom-left-radius: 0;
	border-top: 0;
	border-left: 0;
	border-right: 0;
}

.editor-container_document-editor .editor-container__editor-wrapper {
	max-height: var(--ckeditor5-preview-height);
	min-height: var(--ckeditor5-preview-height);
	overflow-y: scroll;
	background: var(--ck-color-base-foreground);
}

.editor-container_document-editor .editor-container__editor {
	margin-top: 28px;
	margin-bottom: 28px;
	height: 100%;
}

.editor-container_document-editor .editor-container__editor .ck.ck-editor__editable {
	box-sizing: border-box;
	min-width: calc(210mm + 2px);
	max-width: calc(210mm + 2px);
	min-height: 297mm;
	height: fit-content;
	padding: 20mm 12mm;
	border: 1px hsl(0, 0%, 82.7%) solid;
	background: hsl(0, 0%, 100%);
	box-shadow: 0 2px 3px hsla(0, 0%, 0%, 0.078);
	flex: 1 1 auto;
	margin-left: 72px;
	margin-right: 72px;
}




.main-container {
	font-family: 'Lato';
	width: fit-content;
	margin-left: auto;
	margin-right: auto;
}

.ck-content {
	font-family: 'Lato';
	line-height: 1.6;
	word-break: break-word;
}

.ck.ck-toolbar.ck-toolbar_grouping > .ck-toolbar__items {
    /* flex-wrap: nowrap; */
	flex-wrap: wrap !important;
}
.editor-container_classic-editor .editor-container__editor {
	/* min-width: 795px;
	max-width: 795px; */
	min-width: auto;
	max-width: auto;	
}

 

 

 

 

 

3.CkCustomEditor.tsx

 

import React, { useState, useEffect, useRef } from 'react';
import { CKEditor } from '@ckeditor/ckeditor5-react';

import {
	ClassicEditor,
	DecoupledEditor,
	AccessibilityHelp,
	Alignment,
	Autoformat,
	AutoLink,
	Autosave,
	BalloonToolbar,
	BlockQuote,
	BlockToolbar,
	Bold,
	Code,
	Essentials,
	FindAndReplace,
	FontBackgroundColor,
	FontColor,
	FontFamily,
	FontSize,
	GeneralHtmlSupport,
	Heading,
	Highlight,
	HorizontalLine,
	HtmlComment,
	HtmlEmbed,
	Indent,
	IndentBlock,
	Italic,
	Link,
	Mention,
	Paragraph,
	RemoveFormat,
	SelectAll,
	ShowBlocks,
	SpecialCharacters,
	SpecialCharactersArrows,
	SpecialCharactersCurrency,
	SpecialCharactersEssentials,
	SpecialCharactersLatin,
	SpecialCharactersMathematical,
	SpecialCharactersText,
	Strikethrough,
	Subscript,
	Superscript,
	Table,
	TableCaption,
	TableCellProperties,
	TableColumnResize,
	TableProperties,
	TableToolbar,
	TextPartLanguage,
	TextTransformation,
	Underline,
	Undo,
	Image, ImageToolbar, ImageUpload, ImageResize, ImageInsert ,
	CodeBlock, SourceEditing,
	MediaEmbed,
} from 'ckeditor5';

import translations from 'ckeditor5/translations/ko.js';

import 'ckeditor5/ckeditor5.css';

import "@/assets/css/ckeditor.css";
import ckeditorUpload from '@/actions/common/ckeditorUpload';


const CK_EDITOR_TYPE="Article";



// upload Adapter
function uploadAdapter(loader: any) {
	return {
	  upload: () => {
		return new Promise(async (resolve, reject) => {
		  const fd = new FormData();
		  loader.file.then(async (file: File) => {
			fd.append('uploads', file);
  
			try {
			  const imageUrls = await ckeditorUpload(fd);
			  console.log('upload completed fd :', imageUrls);
  
			  if (!imageUrls || imageUrls.length === 0) {
				console.error('No image URLs returned from ckeditorUpload');
				reject(new Error('No image URLs returned'));
				return;
			  }
  
			  resolve({ default: imageUrls[0] }); // 필요한 경우 URL을 적절하게 반환
			} catch (error) {
			  console.error('Upload failed:', error);
			  reject(error);
			}
		  });
		});
	  },
	};
  }
  
  // uploadPlugin 함수 수정
  function uploadPlugin(editor: any) {
	editor.plugins.get('FileRepository').createUploadAdapter = (loader: any) => {
	  return uploadAdapter(loader);
	};
  }

interface CkeditorCustomProps {
	description: string;
	setDescription: (value: string) => void;
  }

const CkCustomEditor:React.FC<CkeditorCustomProps> =
({description, setDescription}) =>{
	
	const editorContainerRef = useRef(null);
	const editorMenuBarRef = useRef(null);
	const editorToolbarRef = useRef(null);
	const editorRef = useRef(null);
	const [isLayoutReady, setIsLayoutReady] = useState(false);

	useEffect(() => {
		setIsLayoutReady(true);

		return () => setIsLayoutReady(false);
	}, []);

	const editorConfig = {
		toolbar: {
		    items: [
				'undo', 'redo', '|', 'showBlocks', '|', 'heading', '|',
				'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', '|',
				'bold', 'italic', 'underline', '|', 'link', 'insertTable', 'highlight', 'blockQuote', '|',
				'alignment', '|', 'outdent', 'indent', 'imageUpload', 'mediaEmbed', 'sourceEditing', 'selectAll', 'codeBlock', 'htmlEmbed', 'accessibilityHelp'
			],
			shouldNotGroupWhenFull: false
		},
		plugins: [
			AccessibilityHelp,
			Alignment,
			Autoformat,
			AutoLink,
			Autosave,
			BalloonToolbar,
			BlockQuote,
			BlockToolbar,
			Bold,
			Code,
			Essentials,
			FindAndReplace,
			FontBackgroundColor,
			FontColor,
			FontFamily,
			FontSize,
			GeneralHtmlSupport,
			Heading,
			Highlight,
			HorizontalLine,
			HtmlComment,
			HtmlEmbed,
			Indent,
			IndentBlock,
			Italic,
			Link,
			Mention,
			Paragraph,
			RemoveFormat,
			SelectAll,
			ShowBlocks,
			SpecialCharacters,
			SpecialCharactersArrows,
			SpecialCharactersCurrency,
			SpecialCharactersEssentials,
			SpecialCharactersLatin,
			SpecialCharactersMathematical,
			SpecialCharactersText,
			Strikethrough,
			Subscript,
			Superscript,
			Table,
			TableCaption,
			TableCellProperties,
			TableColumnResize,
			TableProperties,
			TableToolbar,
			TextPartLanguage,
			TextTransformation,
			Underline,
			Undo,
			Image,           // 이미지 플러그인 추가
			ImageToolbar,    // 이미지 툴바 플러그인 추가
			ImageUpload,     // 이미지 업로드 플러그인 추가
			ImageResize,     // 이미지 리사이즈 플러그인 추가
			ImageInsert,     // 이미지 삽입 플러그인 추가
			CodeBlock,
			SourceEditing,
			MediaEmbed,
			FontFamily,
		],
		image: {
			toolbar: [
				'imageTextAlternative', 
				'imageStyle:inline', 
				'imageStyle:block', 
				'imageStyle:side', 
				'|',
				'toggleImageCaption', 
				'resizeImage:50', 
				'resizeImage:75', 
				'resizeImage:original'
			]
		},
		balloonToolbar: ['bold', 'italic', '|', 'link'],
		blockToolbar: [
			'fontSize',
			'fontColor',
			'fontBackgroundColor',
			'|',
			'bold',
			'italic',
			'|',
			'link',
			'insertTable',
			'|',
			'outdent',
			'indent'
		],
		fontFamily: {
			options: [

				'Noto Sans KR, sans-serif',
				'Nanum Gothic, sans-serif',
				'Nanum Gothic Coding, monospace',
				'Nanum Myeongjo, serif',
				'Nanum Pen Script, cursive',
				'Noto Serif KR, serif',
				'Song Myung, serif',
				'Black Han Sans, sans-serif',
				'Dongle, sans-serif',
				'Arial, Helvetica, sans-serif',
				'Courier New, Courier, monospace',
				'Georgia, serif',
				'Lucida Sans Unicode, Lucida Grande, sans-serif',
				'Tahoma, Geneva, sans-serif',
				'Times New Roman, Times, serif',
				'Trebuchet MS, Helvetica, sans-serif',
				'Verdana, Geneva, sans-serif',
				
			],
			supportAllValues: true
		},
		fontSize: {
			options: [10, 12, 13, 14, 15, 'default',17, 18, 19, 20, 21, 22,23,24,25],
			supportAllValues: true
		},
		heading: {
			options: [
				{
					model: 'paragraph',
					title: 'Paragraph',
					class: 'ck-heading_paragraph'
				},
				{
					model: 'heading1',
					view: 'h1',
					title: 'Heading 1',
					class: 'ck-heading_heading1'
				},
				{
					model: 'heading2',
					view: 'h2',
					title: 'Heading 2',
					class: 'ck-heading_heading2'
				},
				{
					model: 'heading3',
					view: 'h3',
					title: 'Heading 3',
					class: 'ck-heading_heading3'
				},
				{
					model: 'heading4',
					view: 'h4',
					title: 'Heading 4',
					class: 'ck-heading_heading4'
				},
				{
					model: 'heading5',
					view: 'h5',
					title: 'Heading 5',
					class: 'ck-heading_heading5'
				},
				{
					model: 'heading6',
					view: 'h6',
					title: 'Heading 6',
					class: 'ck-heading_heading6'
				}
			]
		},
		htmlSupport: {
			allow: [
				{
					name: /^.*$/,
					styles: true,
					attributes: true,
					classes: true
				}
			]
		},
		initialData:
			'',
		language: 'ko',
		link: {
			addTargetToExternalLinks: true,
			defaultProtocol: 'https://',
			decorators: {
				toggleDownloadable: {
					mode: 'manual',
					label: 'Downloadable',
					attributes: {
						download: 'file'
					}
				}
			}
		},
		mention: {
			feeds: [
				{
					marker: '@',
					feed: [
						/* See: https://ckeditor.com/docs/ckeditor5/latest/features/mentions.html */
					]
				}
			]
		},
		menuBar: {
			isVisible: true
		},
		placeholder: '내용을 입력해 주세요!',
		table: {
			contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties']
		},
		translations: [translations],
		extraPlugins: [uploadPlugin],
	};


	
	// @ts-ignore
	if(CK_EDITOR_TYPE==="Document"){

		return (

			<div>
				<div className="main-container">
					<div className="editor-container editor-container_document-editor" ref={editorContainerRef}>
						<div className="editor-container__menu-bar" ref={editorMenuBarRef}></div>
						<div className="editor-container__toolbar" ref={editorToolbarRef}></div>
						<div className="editor-container__editor-wrapper">
							<div className="editor-container__editor">
								<div ref={editorRef}>
										
								  {isLayoutReady && editorToolbarRef.current && editorToolbarRef.current !== null && (
										
										<CKEditor
										
											onReady={(editor:any) => {
												// @ts-ignore
												editorToolbarRef.current.appendChild(editor.ui.view.toolbar.element);
												// @ts-ignore
												editorMenuBarRef.current.appendChild(editor.ui.view.menuBarView.element);
											}}
											onAfterDestroy={() => {
												// @ts-ignore
												Array.from(editorToolbarRef.current.children).forEach(child => child.remove());
												// @ts-ignore
												Array.from(editorMenuBarRef.current.children).forEach(child => child.remove());
											}}
									
											editor={DecoupledEditor}
											// @ts-ignore
											config={editorConfig}
	
											onChange={(event, editor) => {
												const data = editor.getData();
												setDescription(data);
											  }}
											data={description}
										/>
									)}
								</div>
							</div>
						</div>
					</div>
				</div>
			</div>
		);


	}else if(CK_EDITOR_TYPE==="Article"){
		return (
			<div>
				<div className="main-container">
					<div className="editor-container editor-container_classic-editor" ref={editorContainerRef}>
						<div className="editor-container__editor">
							
							<div ref={editorRef}>{isLayoutReady && 
								
						<CKEditor 								
									editor={ClassicEditor} 
									// @ts-ignore
									config={editorConfig}																
									onChange={(event, editor) => {																						
										const data = editor.getData();
										setDescription(data);
									}}
                                                                       data={description}
								/>
								
								}</div>
						</div>
					</div>
				</div>
			</div>
		);
	}


}


export default CkCustomEditor;

 

 

 

 

 

 

 

4.ckeditorUpload.ts

이미지 업로드는   console.cloudinary.com 에 업로드하는 방법으으로 했다.

 

"use server";
import cloudinary from "@/config/cloudinary";

async function ckeditorUpload(formData: FormData) {
  // 유효성 체크
  const images = formData.getAll("uploads");

  const imageUrls: string[] = [];

    // 다음은  https://console.cloudinary.com/  에 사이트에 이미지를 저장하는 코드 
  for (const imageFile of images) {
    if (imageFile instanceof File) {
      const imageBuffer = await imageFile.arrayBuffer();
      const imageArray = Array.from(new Uint8Array(imageBuffer));
      const imageBase64 = Buffer.from(imageArray).toString("base64");

      const result = await cloudinary.uploader.upload(
        `data:image/png;base64,${imageBase64}`,
        { folder: "propertypulse" }
      );
      imageUrls.push(result.secure_url);
    }
  }

  // 배열을 보장하기 위해 빈 배열을 반환할 수 있음
  console.log("3.imageUrls*****************", imageUrls);
  return imageUrls;
}

export default ckeditorUpload;

 

 

 

 

5. 적용하기

PropertyAddForm.tsx

 

다음과 같이 반드시 서버 사이드 렌더링을 하지 않도록 CkCustomEditor 를 다이나믹 방식으로 임포트 해야 한다.

 

 

"use client";
import addProperty from "@/actions/mongodb/properties/addProperty";
import React, { FormEvent, useState } from "react";
import DaumPostcodeEmbed from "react-daum-postcode";
import { ClipLoader } from 'react-spinners';

// 서버 사이드 렌더링을 하지 않음
import dynamic from 'next/dynamic';
const  CkCustomEditor= dynamic(() => import('@/components/ckediotr/CkCustomEditor'), {
  ssr: false, 
});



const PropertyAddForm:React.FC = () => {

~



~

~
        <CkCustomEditor description={description} setDescription={setDescription}/> 



~

 

 

 

 

 

 

 

6.적용한 결과

 

 

 

소스:

https://github.dev/braverokmc79/nextjs-property-plus

 

 

 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

정신의 칼날을 가는 일을 게을리해서는 안 된다.

댓글 ( 0)

댓글 남기기

작성