상세 화면 들어가기 전 - Toast UI Viewer 적용

손대중·2022년 8월 16일
1

원래는 Article 생성시 본문을 Markdown 원본으로 저장하려고 했다.

근데 굳이 한번 파싱된 HTML 로 저장한 이유는 해당 html 을 곧바로 넣어주면 Toast UI Editor 우측에 나온 것처럼 화면에 나오길 기대해서이다.

근데 실제로 넣어보니 저대로 안나옴;;;

Toast UI Viewer 를 사용해서 의도대로 이쁘게 나오도록 만들자. (https://nhn.github.io/tui.editor/latest/tutorial-example04-viewer 참조)

추가로 처음 생각한대로 Article 본문을 Markdown 원본으로 저장하자.

  • 기존 Article 본문을 Markdown 원본으로 대체
  • Admin 화면 내 Article 생성 & 수정시 본문을 Markdown 원본으로 저장하도록 수정
  • Toast UI Viewer Wrapper 컴포넌트 추가
    • Article 상세 화면에 Viewer 적용 (간단 확인용)

기존 Article 본문을 Markdown 원본으로 대체

기존 HTML 을 다시 Markdown 원본으로 바꿔야 하는 문제가 있으니 백엔드에서 하지 말고 프론트에서 Toast UI Editor 를 사용하자.

백엔드에서 하기 귀찮기도 하고, 기존 HTML 변환을 Toast UI Editor 에서 했으니 Markdown 변환도 Toast UI Editor 에서 하는게 안전해 보여서... (Toast UI Editor 는 백엔드용이 아님.)

뭐 어쨌든 Toast UI Editor Wrapper 컴포넌트에는 Markdown 본문을 get / set 할 수 있도록 getMarkdown, setMarkdown 함수를 추가하자.

  • ToastUIEditor.svelte

    export function getMarkdown() {
        editorInstance.setMarkdown(editorInstance.getMarkdown().trim());
        return editorInstance.getMarkdown();
    }
    
    export function setMarkdown(contents) {
        editorInstance.setMarkdown(contents);
    }

변환 작업은 Toast UI Editor 를 사용할 수 있는 Admin Article 생성 화면에서 하자.

  • AdminAddArticle.svelte - 임시 코드

    const fix = async () => {
        const response = await getArticle();
    
        const arr = [];
    
        for (let i = 0; i < response.data.length; i++) {
            const article = response.data[i];
    
            const contents = editor.getMarkdown(editor.setHTML(article.contents));
    
            arr.push(new Promise(resolve => {
                patchArticle({
                    query: {
                        _id: article._id
                    },
                    data: {
                        contents: contents
                    }
                }).then(() => {resolve();});
            }));
        }
    
        await new Promise.all(arr).then();
    };

이걸로 DB 에 있는 모든 Article 의 본문을 Markdown 으로 변환했다.

Admin 화면 내 Article 생성 & 수정 로직 수정

뭐 제목은 거창하지만 결국 기존에 쓰던 ToastUIEditor.sveltesetHTMl, getHTML 대신에 getMarkdown, setMarkdown 을 사용하도록 수정했다.

Toast UI Viewer Wrapper 컴포넌트 추가

Viewer 는 말그대로 Showing 동작만 하면 되기 때문에 별 다른 동작은 필요 없음.

  • ToastUIViewer.svelte

    <div bind:this={viewer} ></div>
    
    <script>
        import { onMount } from 'svelte';
        import '@toast-ui/editor/dist/toastui-editor.css';
        import '@toast-ui/editor/dist/theme/toastui-editor-dark.css';
    
        import Viewer from '@toast-ui/editor/dist/toastui-editor-viewer';
    
        export let initContents = '';
    
        let viewerInstance;
    
        let viewer;
    
        onMount(() => {
            viewerInstance = new Viewer({
                el: viewer,
                // height: '500px',
                initialValue:initContents,
                theme: 'dark',
            });
    
            viewerInstance.setMarkdown(initContents);
        });
    </script>

Article 상세 화면에 ToastUIViewer.svelte 를 적용해서 잘 나오는지 확인해보자.

  • ArticleDetail.svelte

    <h1>상세화면~~</h1>
    
    {#if !!article.contents}
    <ToastUIViewer initContents={article.contents}></ToastUIViewer>
    {/if}
    
    <script>
    
        import { getArticle } from '../../api/article.js';
        import { location } from 'svelte-spa-router';
    
        import ToastUIViewer from '../external-wrapper/ToastUIViewer.svelte';
    
        let article = {};
    
        const init = async () => {
            const _id = $location.split('/').pop();
    
            const response = await getArticle({ _id });
    
            if (response.success) {
                article = response.data[0];
    
                console.log(article);
            }
        };
    
        location.subscribe(() => {
            init();
        });
    </script>

의도대로 잘 나온다 ㅎㅎ


추가 수정

Article List Item 에서는 대표 이미지와 본문 요약을 HTML 데이터를 파싱해서 쓰고 있었다. (https://velog.io/@crazydj/%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%A7%81%EC%A0%91-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EB%8C%80%EC%9E%91%EC%A0%84-ArticleListItem.svelte 참조)

본문 데이터가 HTML 에서 Markdown 으로 바뀌었기 때문에, 파싱하는 로직을 바꾸어야 한다.

근데 생각해보니... 이런 로직들... util 적인 성격을 띄는 로직들은 별도로 빼 놓는게 좋을 것 같아서 util 로직들을 한 곳에 - src/utils/common.js 에 모아두기로 결정했다.

일단은 기존 HTML 파싱 로직을 옮기자.

  • src/utils/common.js

    // article contents (html) 에서 image 및 summery 추출
    const makeSummeryFromHtml = contents => {
        let image = '';
        let summery = '';
    
        try {
            // article.contents 내에서 첫번째 이미지 src 추출해서 image 로 설정
            const reg = new RegExp(/(<img[^>]+src\s*=\s*[\"']?([^>\"']+)[\"']?[^>]*>)/, 'i');
            const regResult = reg.exec(contents);
            if (regResult?.length > 1) {
                image = regResult[2];
            }
    
            // <img>, <h1~5>, <div>, <blockquote>, <table>, <code> 은 그냥 빈스트링으로 replace
            // <br>, <li> 는 ' ' 로 replace
            // 나머지 태그들 - <~> 전부 빈스트링으로 replace
            summery = contents
                .replace(/(<img[^>]*>)|(<h1[^>]*>.*<\/h1>)|(<h2[^>]*>.*<\/h2>)|(<h3[^>]*>.*<\/h3>)|(<h4[^>]*>.*<\/h4>)|(<h5[^>]*>.*<\/h5>)|(<h6[^>]*>.*<\/h6>)|(<div[^>]*>(?!<div).+?<\/div>)|(<blockquote[^>]*>(?!<blockquote).+?<\/blockquote>)|(<table[^>]*>(?!<table).+?<\/table>)|(<code[^>]*>(?!<code).+?<\/code>)/gi, '')
                .replace(/(<br[^>]*>)|(<li[^>]*>)/gi, ' ')
                .replace(/<[^>]*>?/gi, '')
                .replace(/\n/gi, ' ')
                .slice(0, 200);
        } catch (e) {
            console.error(e);
        }
    
        return { image, summery };
    };
    
    export { makeSummeryFromHtml };

이제 Markdown 을 파싱하는 로직을 추가하자.

Toaust UI Editor 를 쓰고 싶은데, 파싱 로직만 제공하는 건 없는 듯...

해서 Markdown 파싱은 markdonw-it 플러그인을 사용하기로 한다. (https://github.com/markdown-it/markdown-it 참조)

추가로 하는 김에 계속 눈에 거슬렸던 created 날짜 변환 로직도 같이 추가하자.

  • src/utils/common.js

    import MarkdownIt from 'markdown-it';
    
    // article contents (html) 에서 image 및 summery 추출
    const makeSummeryFromHtml = contents => {...};
    
    // article contents (markdown) 에서 image 및 summery 추출
    const mdi = new MarkdownIt();
    const makeSummeryFromMarkdown = contents => {
        let image = '';
        let summery = '';
    
        try {
            const arrContents = mdi
                .parse(contents)
                .filter(content => content.type === 'inline')
                .flatMap(content => content?.children.filter(c => c.type === 'text' || c.type === 'image'));
    
            arrContents.map(content => {
                if (content.type === 'text' && summery.length < 200) {
                    summery += (content.content + ' ');
                } else if (content.type === 'image' && !image) {
                    image = content.attrs[0][1];
                }
            });
        } catch (e) {
            console.error(e);
    
            const result = makeSummeryFromHtml(mdi.render(contents));
            image = result.image;
            summery = result.summery;
        }
    
        return { image, summery };
    };
    
    const getArticleCreated = created => {
        try {
            const date = new Date(created);
            return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일 ${date.getHours()}시 ${date.getMinutes()}분 ${date.getSeconds()}초`;
        } catch (e) {
            console.error(e);
            return '';
        }
    };
    
    export { makeSummeryFromHtml, makeSummeryFromMarkdown, getArticleCreated };

왜인지 모르겠지만... mdi.parse() 호출시 에러가 나는 경우도 있어서... 기존 HTML 파싱하는 로직을 예외처리로 사용했음.

ArticleListItem.svelte 에 추가하는 로직을 아래와 같이 적용하고 잘 동작하는 것까지 확인~.

  • ArticleListItem.svelte

    <script>
        import { makeSummeryFromMarkdown, getArticleCreated } from '../../utils/common.js'
    
        ...
    
        $: {
            const result = makeSummeryFromMarkdown(article.contents);
    
            image = result.image || defaultImage;
            contents = result.summery || 'Contents....';
            created = getArticleCreated(article.created) || '';
        };
    </script>

이제 상세 화면을 개발할 차례다.

그건 다음에....

0개의 댓글