오늘은 기본적인 게시판과 작성을 만들어 볼 것이다.
그냥 input type=textarea 는 너무 심심하니, toast ui editor 를 사용해보려고 한다.
Toast UI Editor는 웹 애플리케이션에서 사용할 수 있는 마크다운 에디터다. 이 에디터는 GitHub-Flavored Markdown(GFM)을 지원하며, WYSIWYG(What You See Is What You Get) 편집과 마크다운 편집 모드를 제공한다.
<th:block th:fragment="toastuiEidtorLib">
기본적으로 toast ui editor 라이브러리와 플러그인을 불러오는 코드들 그리고 그 설정을 위한 script 코드들을 따로 모아서 fragment 로 만들어 줬다. ( 그 내용이 너무나 길어서 블로그에 다 옮겨적기엔 무리다... ㅠ)
이제 editor 가 필요한 곳에선 fragment 만 샥 replace 해주면 된다!
<div class="flex-1 flex items-center justify-center" layout:fragment="content">
<th:block th:replace="common/fragments :: toastuiEidtorLib"></th:block>
게시물 작성을 위한 템플릿의 일부분이다 이 얼마나 아름다운가!
코드 단 한줄로 나는 toast ui editor 를 불러와서 쓸 수 있게 되었다! ㅎㅎ
본문에 이미지도 넣을 수 있는데 이 이미지를 처리하는 과정만 간단히 다뤄보겠다.
const editor = new toastui.Editor({
el: node,
previewStyle: "vertical",
initialValue: initialValue,
height: $node.attr('height') ?? "100%",
language: "ko-KR",
useCommandShortcut: true,
hooks: {
addImageBlobHook: function (blob, callback) {
const csrfToken = $("meta[name='_csrf']").attr("content");
const csrfHeader = $("meta[name='_csrf_header']").attr("content");
const formData = new FormData();
formData.append('file', blob);
fetch('/usr/genFile/temp', {
method: 'POST',
headers: {
'Accept': 'application/json',
[csrfHeader]: csrfToken
},
body: formData
}).then(function (response) {
return response.json();
}).then(function (myJson) {
callback(myJson.data + "?type=temp");
});
}
},
위의 코드는 editor 를 초기화하고 설정하는 코드의 일부분이다.
editor 를 통해 본문을 작성할때, 사진파일을 삽입하게 되면 발생하는 동작을 커스터마이즈할 수 있는 훅을 정의했다.
addImageBlobHook 은 fetch 함수를 사용하여 서버에 post 요청으로 이미지 파일을 보낸다.
서버의 엔드포인트에서는 이미지파일을 받아 임시경로에 파일을 저장하고 그 url 을 응답한다.
서버로부터 응답을 받으면 JSON 형태로 변환하고, callback 함수를 호출하여 이미지의 URL을 에디터에 전달한다.
이렇게 하면 사용자는 editor 를 통해 본문을 작성하는 중에도 이미지를 업로드하면 즉각 이미지가 삽입된 본문의 미리보기가 가능하다! 이 얼마나 친절한 사이트인가!
이번엔 toast ui editor 를 통해 작성 된 본문을 저장해보겠다.
<input type="hidden" name="body">
<input type="hidden" name="bodyHtml">
비밀스러운 input 을 만들어주고
const editor = $(form).find(".toast-ui-editor").data("data-toast-editor");
const markdown = editor.getMarkdown().trim();
const html = editor.getHTML().trim();
form.body.value = markdown;
form.bodyHtml.value = html;
form submit 시에 editor 로 부터 본문을 받아와 값을 넣어준다. 저장시에는 마크다운 문법과 html 형식 두가지 버전으로 저장을 해준다. ( 수정시에는 작성했던 마크다운도 돌려줘야되니까! )
또 최종적으로 서버에 form 을 제출하고 서버에서 본문을 저장하는 로직을 실행할 때, 위에서 업로드했던 임시경로(temp)의 이미지와 최종적으로 제출 된 본문의 이미지를 비교하여 실제 본문에 반영된 이미지만을 실제 경로(Article)로 이동시켜준다.
마지막으로
default String getBodyForEditor() {
return getBody()
.replaceAll("(?i)(</?)script", "$1t-script");
}
XSS 공격을 막기 위해 script 를 t-script 로 치환해서 리턴해주는 함수와
default String getBodyHtmlForPrint() {
return getBodyHtml()
.replace("toastui-editor-ww-code-block-highlighting", "");
}
html 결과물에서 특정 클래스를 제거해서 뷰어에 쓸데없는 버튼이 보이는 문제를 해결해 줄 함수도 만들었다.
오늘도 참 뿌듯한 하루다! ㅎㅎ