Follow Work/SpringbootBoard

[StringBoot] 마크다운 (26)

ReCode.B 2022. 8. 18. 00:46
728x90

참고 링크 : https://wikidocs.net/book/7601

오라클DB와 intelliJ로 작업하였습니다


질문이나 답변을 작성할 때 일반적인 텍스트 외에 글자를 진하게 표시하거나 링크를 추가하고 싶을 수도 있다.

"마크다운"이라는 글쓰기 도구를 이용하면 이런 것들을 쉽고 간단하게 표현할 수 있다.


마크다운 문법

 

리스트

목록을 표시하기 위해 다음처럼 작성한다.

* 자바
* 스프링부트
* 알고리즘

위 문자열을 마크다운 해석기가 HTML로 변환하면 실제 화면에서는 다음과 같이 보인다.

변환결과

  • 자바
  • 스프링부트
  • 알고리즘

마크다운 해석기는 조금 후에 설치하고 실습할 것이니 우선은 마크다운 문법을 익혀보자.

순서가 있는 목록을 표시하려면 다음처럼 작성한다.

1. 하나
1. 둘
1. 셋

 

변환결과

  1. 하나

강조

작성한 글자를 강조 표시하려면 강조할 텍스트 양쪽에 **를 넣어 감싼다.

스프링부트는 **자바**로 만들어진 웹 프레임워크이다.

변환결과

 

변환결과스프링부트는 자바로 만들어진 웹 프레임워크이다.


링크

HTML 링크는 다음처럼 [링크명](링크주소) 규칙을 적용하여 생성한다.

스프링 홈페이지는 [https://spring.io](https://spring.io) 입니다.

변환결과

 

변환결과스프링 홈페이지는 https://spring.io 입니다.


소스코드

소스코드는 백쿼트 ` 3개를 연이어 붙여 위아래로 감싸면 생성할 수 있다.

백쿼트는 백틱이라고도 한다.

```
package com.mysite.sbb;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HelloController {
    @RequestMapping("/hello")
    @ResponseBody
    public String hello() {
        return "Hello Spring Boot Board";
    }
}
```

 

변환결과

package com.mysite.sbb;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HelloController {
    @RequestMapping("/hello")
    @ResponseBody
    public String hello() {
        return "Hello Spring Boot Board";
    }
}

인용

인용을 표시하려면 다음처럼 >를 문장 맨 앞에 입력하고 1칸 띄어쓰기를 한 다음 인용구를 입력한다.

> 마크다운은 Github에서 사용하는 글쓰기 도구이다.

 

변환결과

마크다운은 Github에서 사용하는 글쓰기 도구이다.

 

마크다운의 보다 자세한 사용방법은 다음 문서를 참고하자.

마크다운 문법 공식 문서: www.markdownguide.org/getting-started

 


마크다운 설치

 

SBB에 마크다운 기능을 추가하려면 마크다운 라이브러리를 설치해야 한다.

다음처럼 build.gradle 파일을 수정하여 마크다운을 설치하자.

(... 생략 ...)
dependencies {
    (... 생략 ...)
    implementation 'org.commonmark:commonmark:0.18.2'
}
(... 생략 ...)

"Refresh Gradle Project"를 실행하여 최신버전인 0.18.2 버전의 commonmark 라이브러리를 설치하자.

*라이브러리 설치 후 로컬서버 재시작도 잊지말자.

build.gradle 파일에서 commonmark의 버전정보가 필요한 이유

지금까지 build.gradle 파일에 필요한 라이브러리를 등록할때 버전을 명시하지 않았다. 하지만 commonmark는 위와 같이 0.18.2라는 버전을 지정해 주어야만 한다. 이 차이는 스프링부트의 라이브러리 관리 방식때문이다. 스프링부트가 내부적으로 관리하는 라이브러리에 포함되면 버전 정보가 필요없고 포함되지 않으면 버전 정보가 필요하다. 즉, commonmark는 스프링부트가 내부적으로 관리하는 라이브러리가 아니다.
스프링부트가 관리하는 라이브러리의 경우 버전 정보를 명시하지 않으면 스프링부트가 가장 궁합이 잘 맞는 버전으로 자동 선택한다. 따라서 라이브러리들의 호환성을 생각한다면 버전 정보는 따로 입력하지 않는 편이 좋다.

 


 

마크다운 컴포넌트

이제 질문이나 답변의 "내용"에 마크다운 라이브러리를 적용해야 한다.

컨트롤러에서 질문이나 답변을 조회한 후에 마크다운 라이브러리를 적용하면 변환된 HTML을 얻을 수 있다.

하지만 여기서는 그보다는 좀 더 범용적으로 사용할 수 있는 마크다운 컴포넌트를 만들고

타임리프 템플릿에서 작성한 마크다운 컴포넌트를 사용하는 방법에 대해서 알아보자.

 

다음처럼 CommonUtil 컴포넌트를 작성하자.

CommonUtil.java

package com.gosari.repick_project.markdown;

import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.springframework.stereotype.Component;

@Component /*빈(bean,자바객체)으로 등록*/
public class CommonUtil {

    //markdown메서드는 마크다운텍스트를 HTML 문서로 변환하여 리턴함
    public String markdown(String markdown) {
        Parser parser = Parser.builder().build();
        Node document = parser.parse(markdown);
        HtmlRenderer renderer = HtmlRenderer.builder().build();
        return renderer.render(document);
    }
}

@Component 애너테이션을 사용하여 CommonUtil 클래스를 생성했다.

이렇게 하면 이제 CommonUtil 클래스는 스프링부트에 의해 관리되는 빈(bean, 자바객체)으로 등록된다.

 

이렇게 빈(bean)으로 등록된 컴포넌트는 템플릿에서 바로 사용할 수 있다.

CommonUtil 클래스에는 markdown 메서드를 생성했다. markdown 메서드는 마크다운 텍스트를 HTML 문서로 변환하여 리턴한다. 즉, 마크다운 문법이 적용된 일반 텍스트를 변환된(소스코드, 기울이기, 굵게, 링크 등) HTML로 리턴한다.

참고 - commonmark: https://github.com/commonmark/commonmark-java

 

템플릿에 마크다운 적용

이제 질문 상세 템플릿에 마크다운을 적용해 보자.

<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.w3.org/1999/xhtml"
      layout:decorate="~{layout}">


<div layout:fragment="content" class="container my-3">
    <!--질문-->
    <h2 class="border-bottom py-2" th:text="${question.subject}"></h2>
    <div class="card my-3">
        <div class="card-body">

            <!--마크다운 적용-->
            <div class="card-text" th:utext="${@commonUtil.markdown(question.content)}"></div>
<!--        <div class="card-text" style="white-space: pre-line;" th:text="${question.content}"></div>-->

            <div class="d-flex justify-content-end">
                <!--질문 수정일시표시-->
                <div th:if="${question.modifyDate != null}" class="badge bg-light text-dark p-2 text-start mx-3">
                    <div class="mb-2">modified at</div>
                    <div th:text="${#dates.format(question.modifyDate, 'yyyy-MM-dd HH:mm')}"></div>
                </div>

                <div class="badge bg-light text-dark p-2 text-start">

                    <!--질문글쓴이-->
                    <div class="mb-2">
                        <span th:if="${question.author != null}" th:text="${question.author.username}"></span>
                    </div>
                    <div th:text="${#dates.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></div>

                </div>
            </div>

            <div class="my-3">
                <!--질문추천버튼-->
                <a href="javascript:void(0)"; class="recommend btn btn-sm btn-outline-secondary"
                   th:data-uri="@{|/question/vote/${question.id}|}">추천
                <span class="badge rounded-pill bg-success" th:text="${#lists.size(question.voter)}"></span></a>
                <!--질문수정버튼-->
                <a th:href="@{|/question/modify/${question.id}|}" class="btn btn-sm btn-outline-secondary"
                   sec:authorize="isAuthenticated()"
                   th:if="${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}"
                   th:text="수정"></a>
                <!--질문삭제버튼-->
                <a href="javascript:void(0);" th:data-uri="@{|/question/delete/${question.id}|}"
                  class="delete btn btn-sm btn-outline-secondary" sec:authorize="isAuthenticated()"
                  th:if="${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}"
                  th:text="삭제"></a>
            </div>

        </div>
    </div>
    <!--답변의 갯수표시-->
    <h5 class="border-bottom my-3 py-2"
        th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
    <!--#list.size(이터러블객체)-타임리프가 제공하는 유틸리티로 객체의 길이를 반환-->
    <!--답변 반복 시작-->
    <div class="card my-3" th:each="answer : ${question.answerList}">
        <a th:id="|answer_${answer.id}|"></a> <!--앵커태그-->
        <div class="card-body">

            <!--마크다운 적용-->
            <div class="card-text" th:utext="${@commonUtil.markdown(answer.content)}"></div>
            <!-- <div class="card-text" style="white-space: pre-line;" th:text="${answer.content}"></div>-->

            <div class="d-flex justify-content-end">

                <!--답변 수정일시표시-->
                <div th:if="${answer.modifyDate != null}" class="badge bg-light text-dark p-2 text-start mx-3">
                    <div class="mb-2">modified at</div>
                    <div th:text="${#dates.format(answer.modifyDate, 'yyyy-MM-dd HH:mm')}"></div>
                </div>

                <div class="badge bg-light text-dark p-2 text-start">

                    <!--답변글쓴이-->
                    <div class="mb-2">
                        <span th:if="${answer.author != null}" th:text="${answer.author.username}"></span>
                    </div>

                    <div th:text="${#dates.format(answer.createDate, 'yyyy-MM-dd HH:mm')}"></div>
                </div>
            </div>

            <div class="my-3">
                <!--답변추천버튼-->
                <a href="javascript:void(0);" class="recommend btn btn-sm btn-outline-secondary"
                    th:data-uri="@{|/answer/vote/${answer.id}|}">추천
                <span class="badge rounded-pill bg-success" th:text="${#lists.size(answer.voter)}"></span></a>
                <!--답변수정버튼-->
                <a th:href="@{|/answer/modify/${answer.id}|}" class="btn btn-sm btn-outline-secondary"
                   sec:authorize="isAuthenticated()"
                   th:if="${answer.author != null and #authentication.getPrincipal().getUsername() == answer.author.username}"
                   th:text="수정"></a>
                <!--답변삭제버튼-->
                <a href="javascript:void(0);" th:data-uri="@{|/answer/delete/${answer.id}|}"
                   class="delete btn btn-sm btn-outline-secondary" sec:authorize="isAuthenticated()"
                   th:if="${answer.author != null and #authentication.getPrincipal().getUsername() == answer.author.username}"
                   th:text="삭제"></a>
            </div>

        </div>
    </div>
    <!--답변 반복 끝-->
    <!--답변 작성-->
    <form th:action="@{|/answer/create/${question.id}|}"
          th:object="${answerForm}" method="post" class="my-3">

        <div th:replace="form_errors :: formErrorsFragment"></div>
        <!--th:replace - 공통템플릿을 템플릿내에 삽입가능-->
        <!--div엘리먼트를 form_errors.html파일의 th:fragment속성명이 formErrorsFragment인 엘리먼트로 교체-->


        <textarea sec:authorize="isAnonymous()" disabled th:field="*{content}"
                  class="form-control" rows="10"></textarea>
        <textarea sec:authorize="isAuthenticated()" th:field="*{content}"
                  class="form-control" rows="10"></textarea>

        <input type="submit" value="답변등록" class="btn btn-primary my-2">
    </form>
</div>


<script layout:fragment="script" type="text/javascript">

    <!--질문을삭제할수있는자바스크립트-->
    const delete_elements = document.getElementsByClassName("delete");
    Array.from(delete_elements).forEach(function(element){
        element.addEventListener('click',function (){
            if(confirm("정말로 삭제하시겠습니까?")){
                location.href = this.dataset.uri;
            }
        });
    });
    <!--추천자바스크립트-->
    const recommend_elements = document.getElementsByClassName("recommend");
    Array.from(recommend_elements).forEach(function (element){
        element.addEventListener('click', function(){
            if(confirm("정말로 추천하시겠습니까?")){
                location.href = this.dataset.uri;
            }
        })
    })


</script>

</html>

줄 바꿈을 표시하기 위해 사용했던 기존의 style="white-space: pre-line;" 스타일은 삭제하고 

${@commonUtil.markdown(question.content)}와 같이 마크다운 컴포넌트를 적용했다.

이 때 th:text가 아닌 th:utext를 사용한 부분에 주의하자. 만약 th:utext 대신 th:text를 사용할 경우 HTML의 태그들이 이스케이프(escape)처리되어 태그들이 그대로 화면에 보인다. 마크다운으로 변환된 HTML 문서를 제대로 표시하려면 이스케이프 처리를 하지 않고 출력하는 th:utext를 사용해야 한다.

 

마크다운 확인

이제 질문 또는 답변을 마크다운 문법으로 작성하면 브라우저에서 어떻게 보이는지 확인해 보자.

다음 내용으로 글을 작성해 보자.

**마크다운 문법으로 작성해 봅니다.**

* 리스트1
* 리스트2
* 리스트3

파이썬 홈페이지는 [http://www.python.org](http://www.python.org) 입니다.

위와 같이 마크다운 문법으로 작성한 글은 이제 다음과 같이 보일 것이다.

 

※ 참고: 마크다운 에디터

마크다운 문법을 몰라도 simplemde와 같은 마크다운 UI 도구를 사용하면 마크다운을 보다 쉽게 사용할 수 있다

 

 

728x90