[spring-boot] 블로그를 만들자. (6) 댓글과 Navigation

벌써 6번째 시간이 되었다. 몇 번 남지 않을 듯하다. 그리고 화면은 거의 다 만들었다. 오늘 댓글만 하면 화면에 추가할 내용은 없고 수정할 내용만 조금 있을 듯하다. 그럼 시작해보자.

포스팅에 댓글이 있어야 하므로 post.html에 다음과 같이 추가하자.

<div class="row" style="padding-top: 100px;">
    <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
        <h4 id="addComment">Leave a Comment:</h4>
        <form  th:action="@{/comments}"
               th:object="${commentDto}" method="post" th:id="comment">
            <input type="hidden" th:value="${post.id}" id="postId" name="postId"/>
            <div class="well" th:classappend="(${#fields.hasErrors('content')}? ' has-error')">
                <input type="text" class="form-control input-lg" id="content" name="content"
                       placeholder="content"/>
                <span class="help-block" th:if="${#fields.hasErrors('content')}"
                      th:errors="*{content}"></span>
                <ul class="pager" style="text-align:right;" >
                    <li class="next">
                        <button  type="submit" class="btn btn-primary">Submit</button>
                    </li>
                </ul>
            </div>
        </form>
        <form th:object="${commentDto}" th:action="@{/}" method="post" th:id="deleteComment">
            <ul class="media-list comments" th:each="comment : ${post.comments}">
                <li class="media">
                    <div class="media-body">
                        <h5 class="media-heading pull-left">wonwoo</h5>
                        <div class="comment-info pull-left">
                            <div class="btn-default btn-xs" th:text="${#temporals.format(comment.regDate, 'yyyy-MM-dd')}"><i class="fa fa-clock-o"></i> Posted 3 weeks ago</div>
                        </div>
                        <span style="top: 18px;" th:onclick="'deleteComment(\'' + ${comment.post.id} + '\', \'' + ${comment.id} + '\');'" class="glyphicon glyphicon-remove" ></span>
                        <p class="well" th:text="${comment.content}">This is really awesome snippet!</p>
                    </div>
                </li>
            </ul>
        </form>
    </div>
</div>

포스트의 content밑에 위의 html을 넣으면 된다. 첫 번째 form은 댓글을 입력하는 폼이고 두 번째 폼은 댓글이 존재 한다면 댓글을 보여주고 삭제도 할 수 있는 폼이다. 지금 현재는 권한도 유저도 없다. 로그인을 하면 살짝 바뀔거 같다. 왜냐면 자기가 입력한 것만 삭제 가능하게 만들어야 되니 로그인 기능을 넣으면 html소스가 살짝 변견 될 것이다. 그리고 카테고리랑 포스팅하는 것은 어떤 해당 권한만 할 수 있도록 만들 예정이다. 그래야 블로그니까. 게시판이 아니니 관리자만 입력해야 한다.

<script type="text/javascript" th:inline="javascript">
    function deleteComment(postId, commentId){
        document.getElementById("deleteComment").action = "/comments/"+postId + "/" + commentId;
        document.getElementById("deleteComment").submit();
    }
</script>

그리고 바로 저번 시간에 javascript가 안나와서 div안에 넣었더니 된다고 했었다. thymeleaf가 아직 서툴러서 그냥 대충 했는데 위의 자바스크립트를 <head></head> 헤더 안에 넣어주면 잘 된다. 원래 문법은 head안에 넣는게 맞는거 같은데 body에 넣고 싶다면 저번처럼 해야 되는건가? 아무튼 지금은 head안에 넣으니 잘된다.

위의 html에는 commentDto 이라는 object이 생겼다. 그래서 서버쪽에도 post 페이지로 올때 commentDto를 넣어줘야 한다.
PostController에 @ModelAttribute CommentDto commentDto를 추가 하자. 생각해보니 CommentDto란 말보다 CommentForm이 더 나을 거 같다. 클래스명이…

@GetMapping("/{id}")
public String findByPost(@PathVariable Long id, Model model, @ModelAttribute CommentDto commentDto) {
  //생략
}

위와 같이 추가 되었다면 한번 실행 시켜보자. http://localhost:8080/posts/1 로 접속해보면 아래와 같은 화면이 나올 것이다.
6.blog1

댓글을 입력도 하고 삭제도 해보자. 아주 잘 되는 거 같다. 하지만 댓글을 입력하지 않고 빈값으로 submit을 하면 에러가 발생한다. Exception evaluating SpringEL expression: "post.id" 이런 에러가 발생한다. 무슨 이유일까?
댓글을 입력하는 서버 코드를 보자.

@PostMapping
public String createComment(@ModelAttribute @Valid CommentDto commentDto, BindingResult bindingResult, Model model) {
  if (bindingResult.hasErrors()) {
    return "post/post";
  }
  model.addAttribute("comment", commentService.createComment(
    new Comment(commentDto.getContent(),
      new Post(commentDto.getPostId()))));
  return "redirect:/posts/" + commentDto.getPostId();
}

빈값이 아니고 정상적으로 접근했을 때는 redirect를 하지만 에러가 발생하였을 때는 view를 던진다. 근데 view를 던질 땐 post라는 object이 없다. 아무 object을 던지지 않고 그냥 뷰만 던져서 발생한 에러이다.
여러 방법이 있겠지만 필자의 경우에는 아래와 같이 했다.

private final PostRepository postRepository;

@ModelAttribute
public Post post(@ModelAttribute CommentDto commentDto){
  return postRepository.findOne(commentDto.getPostId());
}

위와 같이 ModelAttribute를 사용해서 post를 던져 주었다. 다시 실행시켜 해보면 아래와 같이 잘 나올 것이다.
6.blog2

다시 한번 입력하고 삭제하고 해보자. 기본적인 화면은 거의 다 완성 되었다. 카테고리, 포스트, 댓글 기본적인 블로그가 완성되었다. 하지만 부족한 부분도 있을 수 있으니 보이면 그때 그때 마다 찾아서 수정 해야겠다. 그 중하나가 상단 백그라운드 이미지 위에 있는 글씨가 맘에 들지 않는다. 지금 현재는 공통으로 빼서 한 글씨만 출력 되고 있다. 각각에 페이지의 이름에 맞게 글씨를 출력 해보자.

6.blog3

위와 같이 Home일 경우에는 Home, 카테고리일 경우에는 Category, 포스트 경우에는 Post라는 글씨를 넣어 볼 예정이다. 물론 각각의 페이지마다 넣어도 되겠지만 그럼 너무 비효율적이다. 각각의 페이지에 맞게 글씨를 넣어보자.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Navigation {

  Section value();
}

위와 같이 Navigation라는 어노테이션을 만들자. 해당 어노테이션은 Section이라는 enum을 value 값으로 넣고 해당 클래스의 value를 뷰에 보내줄때 넣어주면 된다.
아래는 Section enum이다.

public enum Section {
  HOME("Home"),
  POST("Post"),
  CATEGORY("Category");

  private String value;

  Section(String value) {
    this.value = value;
  }

  public String getValue() {
    return value;
  }
}

여기에는 Home, Post, Category만 있지만 다른 프로젝트 혹은 더 추가할 경우에는 Section에 enum을 추가만 해주면 된다.
해당 어노테이션을 사용할 클래스를 만들자.

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new HandlerInterceptorAdapter() {
      @Override
      public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                             ModelAndView modelAndView) throws Exception {

        if (handler instanceof HandlerMethod) {
          HandlerMethod handlerMethod = (HandlerMethod) handler;
          Navigation navSection = handlerMethod.getBean().getClass().getAnnotation(Navigation.class);
          if (navSection != null && modelAndView != null) {
            modelAndView.addObject("navSection", navSection.value().getValue());
          }
        }
      }
    });
  }
}

WebConfig 클래스를 만들고 WebMvcConfigurerAdapter 상속받자. 특별한 로직은 없다 요청 받은 클래스의 Navigation 값을 꺼내서 navSection이라는 키에 값을 넣어주면 끝이다. 어려운 로직은 없다고 판단된다. 마지막으로 해야 할 것이 있는데 Controller클래스에 우리가 만들었던 Navigation 어노테이션을 넣어주자.

//...
@Navigation(Section.POST)
public class PostController {

//...
}

//...
@Navigation(Section.CATEGORY)
public class CategoryController {

//...
}

//...
@Navigation(Section.HOME)
public class IndexController {

//...
}

이제 서버쪽은 끝났고 뷰는 조금만 수정해주면 된다. layout/main.html에 보면 우리가 만든 layout 템플릿이 있다. 거기에 header라는 태그를 찾아 아래와 같이 바꾸자.

<header class="intro-header" style="background-image: url('/img/home-bg.jpg')">
    <div class="container">
        <div class="row">
            <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
                <div class="site-heading" >
                    <h1 style="color:#ffffff" th:text="${navSection}">Clean Blog</h1>
                </div>
            </div>
        </div>
    </div>
</header>

다시 서버를 재시작해서 보면 index페이지에는 Home, 카테고리에는 Category, 포스트에는 Post 문자가 있을 것이다.
이렇게 조금씩 부족한 부부은 수정해 나가야 겠다. 나중에 로그인하면서 권한 문제로 인해 뷰의 코드가 조금 바뀔 수 있다. 그거 말고는 기본적인 것은 다 만들었다.

오늘까지의 소스는 여기에 올라가 있다.

  1. [spring-boot] 블로그를 만들자 (1)

  2. [spring-boot] 블로그를 만들자. (2) JPA

  3. [spring-boot] 블로그를 만들자. (3) Category 와 Comment

  4. [spring-boot] 블로그를 만들자. (4) thymeleaf

  5. [spring-boot] 블로그를 만들자. (5) markdown, catetory

  6. [spring-boot] 블로그를 만들자. (6) 댓글과 Navigation

  7. [spring-boot] 블로그를 만들자. (7) 캐시와 에러페이지

  8. [spring-boot] 블로그를 만들자. (8) GitHub login

  9. [spring-boot] 블로그를 만들자. (9) CI 와 배포

[spring-boot] 블로그를 만들자. (5) markdown, catetory

오늘은 글을 쓰기 위한 markdown과 catetory 화면은 만들어보자. markdown을 만들기 전에 catetory 부터 한번 보자.
서버쪽은 다 만들었으니 view만 만들면 될 것같다.
templates/category 라는 폴더를 만들고 list.html 파일을 만들t자. 그리고 아래와 같이 html코드를 넣자.

<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorator="layouts/main">
... //기타 

<div class="container" layout:fragment="content">
    <li class="next">
        <a th:href="@{/categories/new}">write</a>
    </li>
    <table class="table table-striped">
        <thead>
        <tr>
            <th>#</th>
            <th>name</th>
            <th>date</th>
        </tr>
        </thead>
        <tbody>
        <tr th:each="category,index : ${categories.content}">
            <th scope="row" th:text="${index.count}"></th>
            <td><a th:text="${category.name}" th:href="@{'/categories/' + ${category.id} + '/edit'}"></a></td>
            <td th:text="${#temporals.format(category.regDate, 'yyyy-MM-dd')}"></td>
        </tr>
        </tbody>
    </table>
</div>

카테고리를 등록할 수 있는 페이지로 이동하는 버튼이 상단에 있다. 우리가 일반적으로 JSP를 할때 하던 그런 코드와 비슷하다. 카테고리명을 클릭하면 상세 페이지로 이동할 수 있다. 상세 페이지에서는 변경 또는 삭제가 가능하게 만들어야 된다.
일단 새로 등록 할 수 있는 페이지를 만들어 보자.

<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorator="layouts/main">

...//기타

<div class="container" layout:fragment="content">
    <form class="form-horizontal well bs-component col-lg-10 col-lg-offset-1" th:action="@{/categories}"
          th:object="${categoryDto}" method="post" th:id="category">
        <fieldset>
            <div class="form-group" th:classappend="(${#fields.hasErrors('name')}? ' has-error')">
                <label for="name" class="col-lg-2 control-label">카테고리 명</label>
                <div class="col-lg-10">
                    <input type="text" class="form-control" id="name" name="name" th:field="*{name}"
                           placeholder="Title"/>
                    <span class="help-block" th:if="${#fields.hasErrors('name')}"
                          th:errors="*{name}"></span>
                </div>
            </div>
            <div class="form-group">
                <div class="col-lg-10 col-lg-offset-2">
                    <button type="submit" class="btn btn-primary">Submit</button>
                </div>
            </div>
        </fieldset>
    </form>
</div>

위의 코드는 새로 등록 할 수 있는 페이지다. 일단 필자는 등록 페이지 수정페이지 각각 한개씩 만들었다. 물론 등록, 수정 페이지를 한개로 만들 수도 있을 텐데.. 글쎄 뭐가 좋은지는.. 개발자들의 몫이 아닐까 생각된다. 그때 그때 잘 판단해서 하면 될 듯 하다.
여기서는 파라미터가 잘 못 되었을때 에러 메시지를 출력해주는 그런 코드들도 들어있다. th:classappend="(${#fields.hasErrors('name')}? ' has-error')" name이라는 필드에 에러가 발생하면 has-error라는 class를 넣는 코드이다. 그리고 th:if="${#fields.hasErrors('name')}" th:errors="*{name} 이것 또한 마찬가지로 name이라는 필드에 에러가 있다면 메시지를 출력해주는 그런 코드이다. 한번 테스트를 해보자. 서버를 시작 후에 http://localhost:8080/categories/new 로 접속해보자.

5.blog2

위의 화면은 등록할 때의 페이지다. 뭐 나쁘지않다. 화면도.. 다음은 아무것도 입력하지 않았을때의 화면이다.

5.blog3

다음은 등록을 하고 다음의 모습이다.

5.blog4

아주 깔끔하다. 다음으로는 수정 페이지를 만들어보자. 수정페이지도 등록페이지랑 거의 비슷하다.

<div class="container" layout:fragment="content">
    <form class="form-horizontal well bs-component col-lg-10 col-lg-offset-1" th:action="@{'/categories/'+${id}+'/edit'}"
          th:object="${categoryDto}" method="post" th:id="category">
        <fieldset>
            <div class="form-group" th:classappend="(${#fields.hasErrors('name')}? ' has-error')">
                <label for="name" class="col-lg-2 control-label">카테고리 명</label>
                <div class="col-lg-10">
                    <input type="text" class="form-control" id="name" name="name" th:field="*{name}"
                           placeholder="Title"/>
                    <span class="help-block" th:if="${#fields.hasErrors('name')}"
                          th:errors="*{name}"></span>
                </div>
            </div>
            <div class="form-group">
                <div class="col-lg-10 col-lg-offset-2">
                    <button type="submit" class="btn btn-primary">Submit</button>
                    <input class="btn btn-danger" type="button" value="Delete" th:onclick="'deleteCategory(\'' + ${categoryDto.id} + '\')'"/>
                </div>
            </div>
        </fieldset>
    </form>
    <script type="text/javascript" th:inline="javascript">
        function deleteCategory(categoryId){
            document.getElementById("category").action = "/categories/"+categoryId +"/delete";
            document.getElementById("category").submit();
        }
    </script>
</div>

만들기 나름이겠지만 필자는 위와 같이 만들었다. thymeleaf를 처음 하다보니 모르는게 많다. javascript를 만들고 보니 안된다. 실제 브라우저를 띄워서 개발자 모드로 봐도 javascript가 없다. 그래서 div 안쪽으로 넣었더니 된다. 이게 맞는건지 잘 모르겠으나 일단 되니 그냥 넘어자가. 아마 layout과 관련이 있는거 같다. 저걸로 30분동안 헤멘듯하다. 기존 등록 페이지와 거의 비슷하니 설명은 생략하자. 한번씩 등록하고 수정하고 삭제를 해보자.

이제 카테고리가 완료 되었으니 markdown 에디터를 만들어보자.
여기에 보면 자바스크립트로 누군가 괜찮은 markdown 에디터를 만들어 놨다. 일단 다운 받아서 static 폴더 아래 원하는 부분에 넣자. 필자의 경우에는 아래와 같이 넣었다.
5.blog1

사용하지 않는 파일들은 삭제 했고 폴더 구조도 변경 살짝 변경했다. 그리고 원래는 highlightjs 테마에는 default.css만 있어도 된다. highlightjs 테마들이 여러개 있는데 필자는 그 중에서 github-gist를 선택했다. 그게 제일 이쁜거 같다. 여기 블로그도 gist 테마이다. 다른 테마도 많으니 원하는 테마를 선택해서 넣으면 된다. highlightjs 를 참고 하면 되겠다.

다운받은 markdown 라이브러리에 index.html을 보면 자바스크립트와 css가 길게 있다. 필자는 정신없어서 딴곳으로 뺏다. post.js 와 post.css를 만들어서 정적파일이 모인 static아래 두었다.

    <script th:src="@{/markdown/js/markdown-it.js}"></script>
    <script th:src="@{/markdown/js/markdown-it-footnote.js}"></script>
    <script th:src="@{/markdown/js/highlight.pack.js}"></script>
    <script th:src="@{/codemirror/lib/codemirror.js}"></script>
    <script th:src="@{/codemirror/overlay.js}"></script>
    <script th:src="@{/codemirror/markdown/markdown.js}"></script>
    <script th:src="@{/codemirror/gfm/gfm.js}"></script>
    <script th:src="@{/markdown/js/rawinflate.js}"></script>
    <script th:src="@{/markdown/js/rawdeflate.js}"></script>
    <link rel="stylesheet" th:href="@{/codemirror/css/base16-light.css}"/>
    <link rel="stylesheet" th:href="@{/codemirror/lib/codemirror.css}"/>
    <link rel="stylesheet" th:href="@{/markdown/css/github-gist.css}" />
    <link rel="stylesheet" th:href="@{/css/post.css}" />
    <link th:href="@{/vendor/bootstrap/css/bootstrap.min.css}" rel="stylesheet"/>

스크립트와 css가 많다. 위와 같이 임포트 해주자. 만약 markdown의 테마를 바꾸고 싶다면 github-gist.css 이곳만 변경해주면 된다.

..// 다른 기타 기능

<div id="in">
    <input type="hidden" id="content" name="content" th:field="*{content}"/>
    <input type="hidden" id="code" th:field="*{code}" />
</div>
<div id="out" >
</div>

..// 다른 기타 기능
<div id="menu">
    <input class="btn btn-primary" type="button" value="Submit" onclick="saveAsHtml()"/>
</div>

나머지는 카테고리와 비슷한 코드이므로 소스로 확인하고 여기서 중요한 부분은 in과 out이다. in은 실제 포스팅할 부분이고 out부분은 글을 쓰면 바로바로 마크다운을 확인 할 수 있는 부분이다. 여기서 content와 code 두개가 있는데 code는 원본 code이고 content 경우에는 code를 html 코드로 바꾸어서 들어가는 코드이다. 뭐 물론 content를 서버에서 markdown의 html 코드로 변환시킬수도 있는데 굳이 변환된 코드가 있는데 서버에서 할 필요가 없을 거 같아서 같이 넘겼다.

다운받은 라이브러리에서는 markdown을 다운로드 받을 수 있다. 헌데 우리는 다운받을 필요는 없고 코드만 저장하면 된다.

function saveAsHtml() {
    document.getElementById('content').value = document.getElementById('out').innerHTML;
    document.getElementById('post').submit();
}

위와 같이 out에 있는 코드를 content에 넣어 서버로 주면 된다. 얼추 마크 다운도 완성되었다. 어차피 등록과 수정 페이지는 비슷비슷하니 소스를 참고하면 되겠다.
포스트를 등록할 때 카테고리를 선택해야 되므로 등록이나 수정 페이지로 이동할 때 카테고리 리스트도 가져와야 한다. Post쪽에 다음과 같이 추가하자. 매번 가져 올 필요 없다면 그때 그때 마다 가져와도 되긴하나 일단 필자는 매번 가져오는 방식으로 했다.

@ModelAttribute("categories")
public List<Category> categories(){
  return categoryService.findAll();
}

마크다운쪽은 소스가 길기 때문에 다 넣지 못했다. github에 올려두었으니 참고하면 되겠다.
일단 다 만들었으니 서버를 띄워 http://localhost:8080/posts/new 에 접속해보자.
5.blog5

뭐 그럭저럭 괜찮은 UI가 나왔다. 에디터는 헤더와 푸터를 없앴다. 있어도 안들어가고 화면도 작고 그래서 뺏다. UI 작업은 잘 하지도 못하고 그래서 있는 그대로 넣었다. 거의 대부분 UI는 변경하지 않았다. 위치 정보만 바꾸었고 나머지는 그대로 작업했다.
아래와 같이 markdown을 넣어보자.

spring Boot blog
-----

1. [spring-boot] 블로그를 만들자 (1)
2. [spring-boot] 블로그를 만들자. (2) JPA
3. [spring-boot] 블로그를 만들자. (3) Category 와 Comment
4. [spring-boot] 블로그를 만들자. (4) thymeleaf
5. [spring-boot] 블로그를 만들자. (5) markdown, catetory

### java
```java
class Main {
  public static void main(String[] args){
    System.out.println("Spring Boot Blog");
  }
}
```

### xml
```xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
```

### js
```js
function hello(){
    console.log("hello world");
}
```
### css

```css
sub, sup {
    font-size: 75%;
    line-height: 0;
    position: relative;
    vertical-align: baseline;
}
```

5.blog6

나쁘진 않지만 완전 마음에는 들지 않는다. 하지만 이걸 만들기 위해 엄청나게 고생했다. 아무튼 저장을 해보자. 그럼 아래와 같은 화면이 출력 될 것이다.
5.blog7

여기에는 테마가 적용 되지 않았다. 여기 역시 테마를 적용 시키려면 css를 추가해야 된다. post.html에 css를 추가 시키자.

<link rel="stylesheet" th:href="@{/markdown/css/github-gist.css}" />

그리고 다시 확인해보면 테마가 추가된 화면을 볼 수 있다.
5.blog8

이제 위와 같이 그럭저럭 나쁘지 않은 UI를 볼 수 있다. 블로그를 작성하는 부분이 나와서 그래도 어느정도 나온거 같다.
흠 다음 시간에는 댓글과 캐시? 아니면 github sso 중 하나를 선택해서 해야겠다.
현재까지의 소스는 여기에 올라가 있다.

  1. [spring-boot] 블로그를 만들자 (1)

  2. [spring-boot] 블로그를 만들자. (2) JPA

  3. [spring-boot] 블로그를 만들자. (3) Category 와 Comment

  4. [spring-boot] 블로그를 만들자. (4) thymeleaf

  5. [spring-boot] 블로그를 만들자. (5) markdown, catetory

  6. [spring-boot] 블로그를 만들자. (6) 댓글과 Navigation

  7. [spring-boot] 블로그를 만들자. (7) 캐시와 에러페이지

  8. [spring-boot] 블로그를 만들자. (8) GitHub login

  9. [spring-boot] 블로그를 만들자. (9) CI 와 배포

[spring-boot] 블로그를 만들자. (4) thymeleaf

첫 번째 시간에 말했듯이 thymeleaf 버전은 2.1이다. Spring Boot1.4인 경우에는 thymeleaf3 도 지원 하지만 thymeleaf 서드파트 라이브러리가 아직 많이 지원하지 않는거 같아서 일단 2.1로 했다. 그리고 다른거 templates 보다 자료들이 더 많은거 같아서 thymeleaf로 정했다.
Spring Boot 경우에는 따로 설정할 필요 없다. 클래스패스에 라이브러리만 있다면 boot의 자동설정이 알아서 기본설정을 해준다.
아래와 같이 spring-boot-starter-thymeleaf 를 디펜더시 받자.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

간단하게 hello world를 찍어보자.

public class IndexController {       
 @GetMapping("/")        
 public String home(Model model){        
 model.addAttribute("message", "hello world");       
 return "index";         
 }

message라는 키에 hello world를 넣었다.
spring boot의 경우에 resources/templates/ 아래의 경로에는 사용할 view templates 파일들을 넣으면 되고 resources/static 아래에는 정적 파일들을 넣으면 된다. 우리는 thymeleaf라는 view templates을 사용하므로 resources/templates에 html을 넣고 나머지 css나 js, 폰트 경우에는 static아래에 넣으면 된다.
resources/templates/ 아래 index.html만들어서 아래와 같이 넣어 보자.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8" />
</head>
<body>
  <div th:text="${message}">
  </div>
</body>
</html>

실행 시켜 http://localhost:8080으로 접속하면 hello world 라는 페이지를 볼 수 있다. templates2.1인 경우에는 태그가 정확해야 된다. 만약 닫는 태그가 없다면 에러가 발생한다. 그런데 아마도 3.0부터는 그렇게 엄격하지 않았던거 같다. 대충 쓰는 법을 알았으니 이제 만들어보자.
첫번째 시간에도 말했듯이 clean blog 테마를 이용할 것이다. https://startbootstrap.com/template-overviews/clean-blog/에 가서 다운로드 받자.
다운받은 파일을 압축을 푼후에 html 파일 경우에는 templates 아래에 넣고 나머지 css, img, js, vender 폴더은 static 아래에 넣자.
그리고 index.html 파일을 열어서 태크들을 잘 정리하자. 닫히지 않은 태크들을 다 닫아주고 특히 link 태그와 hr 태그 를 유심히 보자.

<link th:href="@{/vendor/bootstrap/css/bootstrap.min.css}" rel="stylesheet" />

와 같이 th 네임스페이스를 넣어도 되고 아니면 그냥 해도 상관은 없을 듯하다. 경로만 잘 맞으면 상관 없지 않을까? 얼추 태그들 정리가 다 되었다면 다시 실행 시켜보자. 만약 안될 경우에는 로그에 어떤 어떤 태그들을 닫히지 않았다고 나오니 로그를 잘 보자.
다시 서버를 실행시켜서 화면을 띄어보자. 그럼 아래와 같이 이쁜 화면이 나올 것이다.
4.blog1

이제 화면도 띄었으니 post를 가져와 보자.
아까 만든 IndexController 를 살짝 수정해보자.

@Controller
@RequiredArgsConstructor
public class IndexController {

  private final PostRepository postRepository;

  @GetMapping("/")
  public String home(Model model, Pageable pageable){
    model.addAttribute("posts", postRepository.findAll(pageable));
    return "index";
  }
}

블로그이니 페이징도 처리 해야 하므로 Pageable을 파라미터로 받으면 알아서 페이징을 해준다. thymeleaf 서드파트 라이브러리중 페이징처리를 해주는 라이브러리가 있는데 그걸 사용할 것이다.

<dependency>
    <groupId>io.github.jpenren</groupId>
    <artifactId>thymeleaf-spring-data-dialect</artifactId>
    <version>2.1.1</version>
</dependency>

위와 같이 라이브러리를 디펜더시 받고 설정만 해주면 된다.

@Bean
public SpringDataDialect springDataDialect() {
  return new SpringDataDialect();
}

이제 html을 보자. html에 보면 Main Content 라는 주석이 있다. 거기에 있던 내용을 아래와 같이 변경하자.

<div class="row">
    <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
        <div class="post-preview" th:each="post : ${posts.content}">
            <a th:href="@{'/posts/' + ${post.id}}">
                <h2 class="post-title" th:text="${post.title}">
                </h2>
            </a>
            <p class="post-meta">Posted by <a href="#">Start Bootstrap</a> <strong th:text="${#temporals.format(post.regDate, 'yyyy-MM-dd')}"> </strong> category <strong th:text="${post.category.name}"></strong></p>
        </div>
    </div>
</div>
<div class="row" style="margin-top: 5%">
    <div class="col-lg-3 col-lg-offset-1 col-md-10 col-md-offset-1">
        <div sd:pagination-summary="">info</div>
    </div>
    <div class="col-lg-5 col-lg-offset-2 col-md-10 col-md-offset-1">
        <nav class="pull-right">
            <ul class="pagination" sd:pagination="full">
                <li class="disabled"><a href="#" aria-label="Previous"><span aria-hidden="true"></span></a></li>
                <li class="active"><a href="#">1 <span class="sr-only">(current)</span></a></li>
            </ul>
        </nav>
    </div>
</div>

container 아래의 row는 실제 post의 제목이 나오는 부분이고 다음 row는 페이징 처리를 하는 부분이다. 페이징 처리부분은 문서에 나온 그대로 했다. 딱히 뭘 안해줘도 되는 듯 하다. 포스팅은 여러개가 나와야 되므로 each문 통해 loop를 돌리면 된다. thymeleaf서드파트 라이브러리중에 java8 datetime을 지원해주는 것이 또 있다. thymeleaf 서드파트 라이브러리가 많은듯하다.

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>

위와 같이 메이븐을 디펜더시 받으면 된다. 딱히 설정은 하지 않아도 spring boot가 클래스 패스에 해당 라이브러리가 존재하면 알아서 설정해준다.
위와 같이 ${#temporals.format(post.regDate, 'yyyy-MM-dd')} 이렇게 사용하면 된다.

jpa를 전혀 모른다면 딱히 의문점이 없는데 jpa를 아주 살짝 안다면 약간 의문점이 들 수 있다. 바로 ${post.category.name} 이부분이다. post의 category를 접근 하는 이부분이 조금 의문점이 들 수도 있다.
다시 jpa로 넘어가보자.

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CATEGORY_ID")
private Category category;

Post 도메인 중 category라는 필드가 있다. 저번에도 잠깐 언급했었지만 fetch = FetchType.LAZY 일 경우에는 지연로딩을 한다고 했다. 지연로딩은 해당 프로퍼티가 사용될 때 실제 쿼리를 날린다. 하지만 조건이 있는데 트랜잭션 안에서만 그게 가능하다. 트랜잭션 안에서는 마음껏 사용해도 되지만 트랜잭션 밖에서 사용하면 에러가 발생한다. 처음 jpa를 사용하면 자주 나오는 exception인 lazyinitializationexception이다. 이방법을 해결하기 위한것은 opensessioninview 이하 osiv 이라는 것인데 이거 또한 설명할 양이 꽤 된다. 간단하게 설명하자면 view까지 영속성을 확장 한다는 의미이다. 하지만 여기서 중요한것은 확장을 한다고 해도 트랜잭션안에서만 변경이 가능하다.
우리는 아무 설정도 하지 않았는데 lazyinitializationexception이 나오지 않았다. 그것은 기본적으로 Spring boot는 opensessioninview를 사용하고 있다. 그래서 에러가 나오지 않았던 것이다.

기본적인 데이터를 넣기위해서 아래와 같이 insert 쿼리를 만들자.

insert into category(ID, NAME, REG_DATE) values(1, 'spring', CURRENT_TIMESTAMP());
insert into category(ID, NAME, REG_DATE) values(2, 'java', CURRENT_TIMESTAMP());

insert into post(ID, TITLE, CODE, CONTENT, STATUS, REG_DATE, CATEGORY_ID) values(1, '테스트', '지금 포스팅은 테스트 포스팅 입니다.', '지금 포스팅은 테스트 포스팅 입니다.', 'Y',CURRENT_TIMESTAMP(), 1);

해당 sql을 resources 아래의 import.sql 파일을 만들면 서버를 실행할 때 해당 sql이 자동으로 실행 된다.
그럼 서버가 실행 되었다면 다시 웹페이지를 띄워 확인해보자. 우리가 원하는 테스트라는 제목으로 포스팅이 한개 되었다. 서버도 완벽하고 뷰도 완벽하다. 그런데 가만보면 조금 불필요한 코드들이 많다. 현재 있는 html 파일을 보면 비슷한 구석이 많다. 헤더와 푸터는 동일하다. 우리는 Main Content만 살짝 살짝 바꾸어 주면된다.

Thymeleaf Layout Dialect 이라는 기능있다. 공통된 부분은 따로 빼고 해당 body만 교체해주는 그런 기능이다. 예전에 필자는 tiles를 많이썼는데 거기에도 이런 기능이 존재 한다.

우리는 spring-boot-starter-thymeleaf 안에는 우리가 사용할 thymeleaf-layout-dialect 가 포함되어 있다. 그래서 따로 디펜더시를 받을 필요는 없다.
/resources/templates/layouts/ 아래 폴더를 만들고 그 아래 main.html을 만들자. 그리고 공통된 부분 헤더와 푸터를 넣어주자. 더 세밀하게 나눈다면 header footer 파일도 나누어도 되지만 여기선 간단하게 헤더 푸터를 main에 다 넣었다. 그리고 content만 바꾸는 방법으로 하였다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
...// 공통 스크립트

... //header html

<div layout:fragment="content">
</div>

... //footer html

위와 같이 xmlns:layout 네임 스페이스를 추가 하고 공통 부분을 제외한 body 부분은 layout:fragment 속성을 사용 하면 된다.

다음은 content 부분이다. 다시 index.html로 가서 공통부분(스크립트, 헤더, 푸터)은 다 삭제하고 우리가 원하는 post부분만 있으면 된다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sd="http://www.thymeleaf.org/spring-data"
      layout:decorator="layouts/main" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
...//기타 meta/title 설정
<body>
  <div class="container" layout:fragment="content">
  ... // 기존 post html
  </div>
</body>

이렇게 완성되었다. 다시 서버를 재시작해보자. 그럼 아까와 동일한 화면이 나올 것이다.
post의 단건 화면도 만들어보자. 기본이 잘 되어 있어 아주 쉽게 만들 수있다. /resources/templates/post 폴더를 만들어서 post.html 파일을 아래와 같이 작성하자.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      layout:decorator="layouts/main" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
...//기타 meta/title 설정

<body>
    <div class="container" layout:fragment="content">
        <div class="row">
            <h1 class="col-lg-10 col-lg-offset-1 col-md-10 col-md-offset-1" style="margin-bottom:3%" th:text="${post.title}">
            </h1>
            <div class="col-lg-10 col-lg-offset-1 col-md-10 col-md-offset-1" th:utext="${post.content}">
            </div>
        </div>
    </div>
</body>
</html>

thymeleaf를 사용해 뷰를 만들어 보았다. 화면이 나오니까 이제 조금 뭘 한듯 하다.
다음시간에는 글을 쓸수있는 markdown을 만들어 보자. 오늘 소스는 여기에 올라가 있다.
1. [spring-boot] 블로그를 만들자 (1)

  1. [spring-boot] 블로그를 만들자. (2) JPA

  2. [spring-boot] 블로그를 만들자. (3) Category 와 Comment

  3. [spring-boot] 블로그를 만들자. (4) thymeleaf

  4. [spring-boot] 블로그를 만들자. (5) markdown, catetory

  5. [spring-boot] 블로그를 만들자. (6) 댓글과 Navigation

  6. [spring-boot] 블로그를 만들자. (7) 캐시와 에러페이지

  7. [spring-boot] 블로그를 만들자. (8) GitHub login

  8. [spring-boot] 블로그를 만들자. (9) CI 와 배포