벌써 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 와 배포