Search
Duplicate

[스프링 게시판] 에디터 적용

@3/15/2023

에디터 적용

마크다운을 지원하는 TOAST UI Editor를 사용하기로 함

적용 방법

CDN 적용
<!-- TOAST UI Editor --> <link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.min.css" />
Java
복사
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script> <!-- 한국어 패치 --> <script src="https://uicdn.toast.com/editor/latest/i18n/ko-kr.min.js"></script>
Java
복사

데이터 얻기

다음처럼 마크다운 또는 HTML형태로 데이터를 얻을 수 있음
editor.getMarkdown(); editor.getHTML();
Java
복사
예시

에디터 내용을 form에 담아 보내기

공식 문서도 그렇고 생각보다 에디터 내용을 form에 담아 보내는 방법이 잘 안 보임
에디터 작성 폼 html
<!-- 글 등록 --> <div class="container"> <form th:action="@{/posts}" method="post"> <!-- 제목 --> <div class="input-group mb-3"> <span class="input-group-text" id="inputGroup-sizing-default">제목</span> <input name="title" type="text" class="form-control" aria-label="title input" /> </div> <!-- 본문 --> <div id="editor"></div> <div class="d-flex"> <button type="button" class="btn btn-md btn-primary ms-auto my-3" onclick="submitPost(this)">글 등록</button> <a class="btn btn-md btn-outline-secondary ms-2 my-3" href="/posts" value="취소">취소</a> </div> <input type="hidden" name="content"> <input type="hidden" name="userId" id="userId" value="1"/> <!-- TODO - 유저 권한 기능 추가 후 변경 --> </form> </div>
Java
복사
Javascript(vanila)
function submitPost(button) { console.log("submit"); let form = button.closest('form'); let inputs = form.getElementsByTagName('input'); for (let input of inputs) { if(input.name === 'content') { input.value = editor.getHTML(); } } form.submit(); }
Java
복사
에디터 내용을 뽑아서 hidden input에 넣은 후 제출하는 방식으로 구현함

출력

출력단에서 HTML이 텍스트 그대로 출력됨
기존 타임리프 본문 출력부분의 text를 unexcaped text(utext)로 바꾸면 렌더링이 가능함
<div th:utext="${post.content}" class="row px-3" style="white-space: pre-wrap"> Lorem ipsum pariatur dolor impedit libero minima eaque velit aperiam molestiae numquam doloribus eveniet et aspernatur placeat. </div>
Java
복사
하지만 이는 게시글 작성자가 HTML, JS 코드 등을 주입해 XSS(cross site scripting) 공격이 가능하게 하는 보안 취약점을 유발함
그래서 이런 HTML 데이터를 사용할 때는 소독(sanitizing)을 해줘야 하는데, 대표적인 라이브러리가 DOMPurify
Toast UI view는 이 DOMPurify를 이용해 보안 취약점을 해결해주면서 출력물을 예쁘게 꾸며줌

Toast UI Editor Viewer 적용 방법(CDN)

CDN - body 태그 안에
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-viewer.js"></script>
HTML
복사
CSS - head 태그 안에
<link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor-viewer.min.css" />
HTML
복사
공식 튜토리얼에 나온 방식 → 순수(바닐라)JS는 이대로 하면 오류남(react, vue 등은 안 해봄)
const viewer = new Viewer({ el: document.querySelector('#viewer'), height: '600px', initialValue: '# hello' });
Java
복사
CDN + 바닐라JS는 공식 튜토리얼에 있는대로 하면 안 되고, 아래 예제에 있는 대로 적용해야 함
html
<div class="viewer"></div>
HTML
복사
JS
//Toast UI Editor viewer const viewer3 = new toastui.Editor({ el: document.querySelector('#viewer'), initialValue: 'hi' });
JavaScript
복사
thymeleaf까지 적용하면
<script th:inline="javascript"> // Toast UI Editor viewer const viewer = new toastui.Editor({ el: document.querySelector('#viewer'), initialValue: [[${post.content}]] }); </script>
HTML
복사

샘플 - Toast UI Editor

<!DOCTYPE html> <html> <head xmlns:th="http://www.thymeleaf.org"> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <!-- TOAST UI Editor CSS --> <link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.min.css" /> </head> <body> <!-- 글 등록 --> <div class="container"> <form th:action="@{/posts}" method="post"> <!-- 제목 --> <div class="input-group mb-3"> <span class="input-group-text" id="inputGroup-sizing-default">제목</span> <input name="title" type="text" class="form-control" aria-label="title input"/> </div> <!-- 본문 --> <div id="editor"></div> <div class="d-flex"> <button type="button" class="btn btn-md btn-primary ms-auto my-3" onclick="submitPost(this)">글 등록 </button> <a class="btn btn-md btn-outline-secondary ms-2 my-3" href="/posts" value="취소">취소</a> </div> <input type="hidden" name="content"> <input type="hidden" name="userId" id="userId" th:value="${user.id}"/> </form> </div> <!-- TOAST UI Editor CDN --> <script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script> <!-- 한국어 패치 --> <script src="https://uicdn.toast.com/editor/latest/i18n/ko-kr.min.js"></script> <script> const Editor = toastui.Editor; const editor = new Editor({ el: document.querySelector('#editor'), height: '500px', initialEditType: 'WYSIWYG', previewStyle: 'vertical', language: "ko-KR" }); function submitPost(button) { console.log("submit"); let form = button.closest('form'); let inputs = form.getElementsByTagName('input'); for (let input of inputs) { if(input.name === 'content') { input.value = editor.getHTML(); } } form.submit(); } </script> </body> </html>
HTML
복사

샘플 - Toast UI Viewer

Thymeleaf, Javascript 사용 시
<!DOCTYPE html> <html> <head xmlns:th="http://www.thymeleaf.org"> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <!-- TOAST UI Editor Viewer CSS --> <link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor-viewer.min.css" /> </head> <body> <!-- Toast Editor Viewer--> <div class="viewer"></div> <!-- Toast Editor Viewer CDN --> <script src="https://uicdn.toast.com/editor/latest/toastui-editor-viewer.js"></script> <script th:inline="javascript"> // Toast UI Editor viewer const viewer = new toastui.Editor({ el: document.querySelector('#viewer'), height: 'auto', initialValue: [[${post.content}]] }); </script> </body> </html>
HTML
복사

결과화면

에디터
뷰어
이미지까지 base64로 인코딩되어 잘 올라가는 것을 볼 수 있음
다만 이미지는 DB에 큰 부담을 줄 수 있기 때문에 별도 업로드 처리를 해줘야함

아무것도 없는 버튼만 눌러도 form이 제출되는 상황

<form th:action="@{/posts}" method="post"> <!-- 제목 --> <div class="input-group mb-3"> <span class="input-group-text" id="inputGroup-sizing-default">제목</span> <input name="title" type="text" class="form-control" aria-label="title input" /> </div> <!-- 본문 --> <div id="editor"></div> <div class="d-flex"> <button class="btn btn-md btn-primary ms-auto my-3">글 등록</button> <a class="btn btn-md btn-outline-secondary ms-2 my-3" href="/posts" value="취소">취소</a> </div> <input type="hidden" name="content"> <input type="hidden" name="userId" id="userId" value="1"/> <!-- TODO - 유저 권한 기능 추가 후 변경 --> </form>
Java
복사
찾아보니 HTML5부터는 버튼이 기본 submit을 내장하고 있다고 함

편집 시 에디터 적용

initialValue에 값을 넣어도 딱히 에디터 창에 넣어주질 않아서 다음 방법으로 설정해줌
editor.setHTML([[${post.content}]]);
SQL
복사
postUdateForm.html
<!-- 글 수정 --> <div class="container"> <form th:action="@{/posts/{postId}/edit(postId = ${post.id})}" method="post"> <!-- 제목 --> <div class="input-group mb-3"> <span class="input-group-text" id="inputGroup-sizing-default">제목</span> <input th:value="${post.title}" name="title" type="text" class="form-control" aria-label="title input"/> </div> <!-- 본문 --> <div id="editor"></div> <div class="d-flex"> <input type="hidden" name="content"> <button class="btn btn-md btn-primary ms-auto my-3" type="button" onclick="submitPost(this)">글 수정 </button> <a class="btn btn-md btn-outline-secondary ms-2 my-3" href="/posts">취소</a> </div> <input type="hidden" name="userId" id="userId" value="1"/> <!-- TODO - 유저 권한 기능 추가 후 변경 --> <input type="hidden" name="id" id="postId" th:value="${post.id}"/> </form> </div> <!-- TOAST UI Editor CDN --> <script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script> <!-- 한국어 패치 --> <script src="https://uicdn.toast.com/editor/latest/i18n/ko-kr.min.js"></script> <script th:inline="javascript"> const Editor = toastui.Editor; const editor = new Editor({ el: document.querySelector('#editor'), height: '500px', initialEditType: 'WYSIWYG', previewStyle: 'vertical', //initialValue: [[${post.content}]]; 적용이 안됨. 원인 불명 language: "ko-KR" }); editor.setHTML([[${post.content}]]); function submitPost(button) { console.log("submit"); let form = button.closest('form'); let inputs = form.getElementsByTagName('input'); for (let input of inputs) { if(input.name === 'content') { input.value = editor.getHTML(); } } form.submit(); } </script>
SQL
복사