toast UI / toast 에디터 적용하기

minjjai·2022년 11월 12일
0

개요

멋북스 개인 프로젝트를 진행하는 과정에서 toast에디터를 프로젝트에 적용해야 하는 상황이었다. 여러가지 면에서 쉽지 않기도 했다. 다음에 또 적용할 때 쉽게 할 수 있도록 기록을 남겨두고자 한다.

삽입 코드

  1. 먼저 토스트 에디터 UI를 적용하기 위해서는 jQuery와 테일윈드css가 필요하다.
    head 혹은 body에 추가해준다.
<!-- 제이쿼리 불러오기 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<!-- 테일윈드 불러오기 -->
<script src="https://cdn.tailwindcss.com"></script>
  1. 토스트 에디터 UI 의존성을 추가해준다.

head에 추가하면 에디터의 툴바가 표시되는 과정에 문제가 생길 수 있다.
body의 상단에 추가해주도록 하자.

<!-- 토스트 UI 에디터 의존성 시작 -->

<!-- 토스트 UI 에디터 코어 -->
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script>
<link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.min.css" />
<link rel="stylesheet" href="https://nhn.github.io/tui.editor/latest/dist/cdn/theme/toastui-editor-dark.css">

<!-- 토스트 UI 컬러피커 -->
<link rel="stylesheet" href="https://uicdn.toast.com/tui-color-picker/latest/tui-color-picker.css" />
<script src="https://uicdn.toast.com/tui-color-picker/latest/tui-color-picker.min.js"></script>

<!-- 토스트 UI 컬러피커와 에디터 연동 플러그인 -->
<link rel="stylesheet" href="https://uicdn.toast.com/editor-plugin-color-syntax/latest/toastui-editor-plugin-color-syntax.min.css" />
<script src="https://uicdn.toast.com/editor-plugin-color-syntax/latest/toastui-editor-plugin-color-syntax.min.js"></script>

<!-- 토스트 UI 에디터 플러그인, 코드 신텍스 하이라이터 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.28.0/themes/prism-okaidia.min.css">
<link rel="stylesheet" href="https://uicdn.toast.com/editor-plugin-code-syntax-highlight/latest/toastui-editor-plugin-code-syntax-highlight.min.css">
<script src="https://uicdn.toast.com/editor-plugin-code-syntax-highlight/latest/toastui-editor-plugin-code-syntax-highlight-all.min.js"></script>

<!-- 토스트 UI 에디터 플러그인, 테이블 셀 병합 -->
<script src="https://uicdn.toast.com/editor-plugin-table-merged-cell/latest/toastui-editor-plugin-table-merged-cell.min.js"></script>

<!-- 토스트 UI 에디터 플러그인, UML -->
<script src="https://uicdn.toast.com/editor-plugin-uml/latest/toastui-editor-plugin-uml.min.js"></script>

<!-- 토스트 UI 차트 -->
<link rel="stylesheet" href="https://uicdn.toast.com/chart/latest/toastui-chart.css">
<script src="https://uicdn.toast.com/chart/latest/toastui-chart.js"></script>
<!-- 토스트 UI 차트와 토스트 UI 에디터를 연결  -->
<script src="https://uicdn.toast.com/editor-plugin-chart/latest/toastui-editor-plugin-chart.min.js"></script>

<!-- katex -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.0/katex.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.0/katex.min.css">

<!-- docpurify -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.3.8/purify.min.js"></script>
<!-- 토스트 UI 에디터 의존성 끝 -->
  1. css 추가
 <style>

        /* 토스트 UI 에디터 관련 스타일 시작 */
        html > body,
        html > body .ProseMirror,
        html > body .toastui-editor-contents,
        html > body code[class*="language-"],
        html > body pre[class*="language-"],
        html > body code[class*="lang-"],
        html > body pre[class*="lang-"] {
            font-family: "RIDIBatang";
            text-underline-position: under;
            letter-spacing: 0;
        }

        html > body code[class*="language-"],
        html > body pre[class*="language-"],
        html > body code[class*="lang-"],
        html > body pre[class*="lang-"] {
            color: white;
            background-color: #444;
        }

        html > body .ProseMirror,
        html > body .toastui-editor-contents {
            font-size: 1.1rem;
        }

        .toastui-editor-dark {
            background-color: #333;
        }

        /* 토스트 UI 에디터 관련 스타일 끝 */
    </style>
  1. 토스트 에디터 관련 script 추가
    console.clear();

    // 토스트 에디터 시작

    // 토스트 에디터 - 라이브러리 - 시작
    function ToastEditor__getUriParams(uri) {
        uri = uri.trim();
        uri = uri.replaceAll("&amp;", "&");
        if (uri.indexOf("#") !== -1) {
            let pos = uri.indexOf("#");
            uri = uri.substr(0, pos);
        }

        let params = {};

        uri.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (str, key, value) {
            params[key] = value;
        });
        return params;
    }

    function ToastEditor__escape(origin) {
        return origin
            .replaceAll("<t-script", "<script")
            .replaceAll("</t-script", "</script");
    }

    function ToastEditor__getAttrValue($el, attrName, defaultValue) {
        const value = $el.attr(attrName);

        if (!value) {
            return defaultValue;
        }

        return value;
    }

    // 토스트 에디터 - 라이브러리 - 끝

    // 토스트 에디터 - 플러그인 - 시작
    const ToastEditor__chartOptions = {
        minWidth: 100,
        maxWidth: 600,
        minHeight: 100,
        maxHeight: 300
    };

    function ToastEditor__PluginYoutube() {
        const toHTMLRenderers = {
            youtube(node) {
                const html = renderYoutube(node.literal);

                return [
                    { type: "openTag", tagName: "div", outerNewLine: true },
                    { type: "html", content: html },
                    { type: "closeTag", tagName: "div", outerNewLine: true }
                ];
            }
        };

        function renderYoutube(uri) {
            uri = uri.replace("https://www.youtube.com/watch?v=", "");
            uri = uri.replace("http://www.youtube.com/watch?v=", "");
            uri = uri.replace("www.youtube.com/watch?v=", "");
            uri = uri.replace("youtube.com/watch?v=", "");
            uri = uri.replace("https://youtu.be/", "");
            uri = uri.replace("http://youtu.be/", "");
            uri = uri.replace("youtu.be/", "");

            let uriParams = ToastEditor__getUriParams(uri);

            let width = "100%";
            let height = "100%";

            let maxWidth = 500;

            if (!uriParams["max-width"] && uriParams["ratio"] == "9/16") {
                uriParams["max-width"] = 300;
            }

            if (uriParams["max-width"]) {
                maxWidth = uriParams["max-width"];
            }

            let ratio = "16/9";

            if (uriParams["ratio"]) {
                ratio = uriParams["ratio"];
            }

            let marginLeft = "auto";

            if (uriParams["margin-left"]) {
                marginLeft = uriParams["margin-left"];
            }

            let marginRight = "auto";

            if (uriParams["margin-right"]) {
                marginRight = uriParams["margin-right"];
            }

            let youtubeId = uri;

            if (youtubeId.indexOf("?") !== -1) {
                let pos = uri.indexOf("?");
                youtubeId = youtubeId.substr(0, pos);
            }

            return (
                '<div style="max-width:' +
                maxWidth +
                "px; margin-left:" +
                marginLeft +
                "; margin-right:" +
                marginRight +
                "; aspect-ratio:" +
                ratio +
                ';" class="relative"><iframe class="absolute top-0 left-0 w-full" width="' +
                width +
                '" height="' +
                height +
                '" src="https://www.youtube.com/embed/' +
                youtubeId +
                '" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>'
            );
        }
        // 유튜브 플러그인 끝

        return { toHTMLRenderers };
    }

    // katex 플러그인
    function ToastEditor__PluginKatex() {
        const toHTMLRenderers = {
            katex(node) {
                let html = katex.renderToString(node.literal, {
                    throwOnError: false
                });

                return [
                    { type: "openTag", tagName: "div", outerNewLine: true },
                    { type: "html", content: html },
                    { type: "closeTag", tagName: "div", outerNewLine: true }
                ];
            }
        };

        return { toHTMLRenderers };
    }

    function ToastEditor__PluginCodepen() {
        const toHTMLRenderers = {
            codepen(node) {
                const html = renderCodepen(node.literal);

                return [
                    { type: "openTag", tagName: "div", outerNewLine: true },
                    { type: "html", content: html },
                    { type: "closeTag", tagName: "div", outerNewLine: true }
                ];
            }
        };

        function renderCodepen(uri) {
            let uriParams = ToastEditor__getUriParams(uri);

            let height = 400;

            let preview = "";

            if (uriParams.height) {
                height = uriParams.height;
            }

            let width = "100%";

            if (uriParams.width) {
                width = uriParams.width;
            }

            if (!isNaN(width)) {
                width += "px";
            }

            let iframeUri = uri;

            if (iframeUri.indexOf("#") !== -1) {
                let pos = iframeUri.indexOf("#");
                iframeUri = iframeUri.substr(0, pos);
            }

            return (
                '<iframe height="' +
                height +
                '" style="width: ' +
                width +
                ';" scrolling="no" title="" src="' +
                iframeUri +
                '" frameborder="no" allowtransparency="true" allowfullscreen="true"></iframe>'
            );
        }

        return { toHTMLRenderers };
    }
    // 유튜브 플러그인 끝

    // repl 플러그인 시작
    function ToastEditor__PluginRepl() {
        const toHTMLRenderers = {
            repl(node) {
                const html = renderRepl(node.literal);

                return [
                    { type: "openTag", tagName: "div", outerNewLine: true },
                    { type: "html", content: html },
                    { type: "closeTag", tagName: "div", outerNewLine: true }
                ];
            }
        };

        function renderRepl(uri) {
            var uriParams = ToastEditor__getUriParams(uri);

            let uriBits = uri.split("#");
            const hash = uriBits.length == 2 ? uriBits[1] : "";
            uriBits = uriBits[0].split("?");

            const newUrl = uriBits[0] + "?embed=true#" + hash;

            var height = 400;

            if (uriParams.height) {
                height = uriParams.height;
            }

            return (
                '<iframe frameborder="0" width="100%" height="' +
                height +
                'px" src="' +
                newUrl +
                '"></iframe>'
            );
        }

        return { toHTMLRenderers };
    }
    // 토스트 에디터 - 플러그인 - 끝

    // 토스트 에디터 - 에디터 초기화 - 시작
    function ToastEditor__init() {
        $(".toast-ui-editor, .toast-ui-viewer").each(function (index, node) {
            const $node = $(node);
            const isViewer = $node.hasClass("toast-ui-viewer");
            const $initialValueEl = $node.find(" > script");
            const initialValue =
                $initialValueEl.length == 0
                    ? ""
                    : ToastEditor__escape($initialValueEl.html().trim());

            const placeholder = ToastEditor__getAttrValue(
                $node,
                "toast-ui-editor--placeholder",
                ""
            );
            const previewStyle = ToastEditor__getAttrValue(
                $node,
                "toast-ui-editor--previewStyle",
                "vertical"
            );
            const height = ToastEditor__getAttrValue(
                $node,
                "toast-ui-editor--height",
                "600px"
            );
            const theme = ToastEditor__getAttrValue(
                $node,
                "toast-ui-editor--theme",
                "light"
            );

            const editorConfig = {
                el: node,
                viewer: isViewer,
                previewStyle: previewStyle,
                initialValue: initialValue,
                placeholder: placeholder,
                height: height,
                theme: theme,
                plugins: [
                    [toastui.Editor.plugin.chart, ToastEditor__chartOptions],
                    [toastui.Editor.plugin.codeSyntaxHighlight, { highlighter: Prism }],
                    toastui.Editor.plugin.tableMergedCell,
                    toastui.Editor.plugin.colorSyntax,
                    [
                        toastui.Editor.plugin.uml,
                        { rendererURL: "http://www.plantuml.com/plantuml/svg/" }
                    ],
                    ToastEditor__PluginKatex,
                    ToastEditor__PluginYoutube,
                    ToastEditor__PluginCodepen,
                    ToastEditor__PluginRepl
                ],
                customHTMLSanitizer: (html) => {
                    return (
                        DOMPurify.sanitize(html, {
                            ADD_TAGS: ["iframe"],
                            ADD_ATTR: [
                                "width",
                                "height",
                                "allow",
                                "allowfullscreen",
                                "frameborder",
                                "scrolling",
                                "style",
                                "title",
                                "loading",
                                "allowtransparency"
                            ]
                        }) || ""
                    );
                }
            };

            const editor = isViewer
                ? new toastui.Editor.factory(editorConfig)
                : new toastui.Editor(editorConfig);

            $node.data("data-toast-editor", editor);
        });
    }
    // 토스트 에디터 - 에디터 초기화 - 끝

    // 토스트 에디터 실행
    ToastEditor__init();

    // 토스트 에디터 끝

    function ArticleSave__submit(form) {
        form.title.value = form.title.value.trim();

        if (form.title.value.length == 0) {
            alert("제목을 입력해주세요");
            form.title.focus();

            return;
        }

        const editor = $(form).find(".toast-ui-editor").data("data-toast-editor");

        const markdown = editor.getMarkdown();
        console.log(markdown);
        form.body.value = markdown.trim();

        if (form.body.value.length == 0) {
            alert("내용을 입력해주세요");
            editor.focus();

            return;
        }

        alert(
            `폼 체크 완료 / title : ${form.title.value} / body : ${form.body.value}`
        );

        // form.submit();
    }
  1. html form 태그를 다음과 같이 추가한다.
<form th:action="@{/article/create}"  method="post" onsubmit="ArticleSave__submit(this); return false;">
        <input type="hidden" name="body" placeholder="내용을 입력해주세요." />
        <div>
            <input type="text" name="title" placeholder="제목을 입력해주세요." class="border">
        </div>

        <div class="toast-ui-editor" toast-ui-editor--height="400px"></div>

        <div>
            <input type="submit" value="작성">
        </div>
</form>

주의 사항

  • input의 name이 body로 되어 있는데, body가 아닌 content나 다른 이름으로 할 경우에는 script문의 Article__submit()함수의 body도 변경해주어야 한다.
  • 원인은 파악하지 못했으나... script문을 form 보다 위에 작성하면 UI가 나타나지 않는다.

결과

위와 같은 코드들을 갖다 붙이기만 하면 아래와 같이 토스트 에디터 UI를 사용할 수 있다.
4번의 html코드를 보면, title, content라는 이름으로 변수값을 넘겨주는 것을 볼 수 있다. 이것은 입맛에 맞게 이름을 정해주면 된다.

profile
BackEnd Developer

0개의 댓글