Last Modified 2022.3.8

게시판 댓글을 RESTful URL로 수정

다음은 댓글 요청 URL을 RESTful URL에 부합하도록 수정한 결과다.

목록:		GET /comments/23	--23은 게시글 고유번호--
새글:		POST /comments/23
수정:		PUT /comments/23/38 	--38은 댓글 고유번호--
삭제:		DELETE /comments/23/38

댓글을 에이잭스 프로그램으로 수정하려 한다.
BbsController와 같은 패키지에 CommentsController를 생성한다.

@RestController
@RequestMapping("comments")
public class CommentsController {

  @Autowired
  private BoardService boardService;
  
  //TODO

}	

@RestController는 @Controller와 @ResponseBody 어노테이션을 합쳐놓은 어노테이션이다. 클래스 선언에 @RestController 어노테이션을 쓰면, 메소드마다 @ResponseBody를 붙여 주지 않아도 된다.

@ResponseBody 어노테이션이 있는 메소드의 반환 값은 뷰를 해석하는 데 사용되는 게 아니라, 메시지 변환기를 거쳐 변환된 후 응답 바디에 바로 쓰인다.

메시지 변환기가 메소드 반환 값을 JSON 객체로 바꾸게 하려면, pom.xml에 다음 의존성을 추가한다.

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.12.0</version>
</dependency>

목록

Comment 클래스에 editable 속성을 추가한다.

package net.java_school.board;

import java.util.Date;

public class Comment {

  private int commentNo;
  private int articleNo;
  private String email;
  private String name;
  private String memo;
  private Date regdate;
  private boolean editable;

  public int getCommentNo() {
    return commentNo;
  }

  public void setCommentNo(int commentNo) {
    this.commentNo = commentNo;
  }

  public int getArticleNo() {
    return articleNo;
  }

  public void setArticleNo(int articleNo) {
    this.articleNo = articleNo;
  }

  public String getEmail() {
    return email;
  }

  public void setEmail(String email) {
    this.email = email;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getMemo() {
    return memo;
  }

  public void setMemo(String memo) {
    this.memo = memo;
  }

  public Date getRegdate() {
    return regdate;
  }

  public void setRegdate(Date regdate) {
    this.regdate = regdate;
  }
  
  public boolean isEditable() {
    return editable;
  }

  public void setEditable(boolean editable) {
    this.editable = editable;
  }
  
}

다음 메소드를 댓글 컨트롤러에 추가한다.

@GetMapping("{articleNo}")
public List<Comment> getAllComments(@PathVariable Integer articleNo, 
  Principal principal, Authentication authentication) {

  List<Comment> comments = boardService.getCommentList(articleNo);

  Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

  boolean isAdmin = false;

  for (GrantedAuthority authority : authorities) {
    String role = authority.getAuthority();
    if (role.equals("ROLE_ADMIN")) {
      isAdmin = true;
      break;
    }
  }

  if (isAdmin) {
    for (Comment comment : comments) {
      comment.setEditable(true);
      comment.setEmail(null);
    }
  } else {
    String username = principal.getName();
    for (Comment comment : comments) {
      if (comment.getEmail().equals(username)) {
        comment.setEditable(true);
      }
    }
    for (Comment comment : comments) {
      comment.setEmail(null);
    }
  }

  return comments;
}

상세보기 페이지에서 GET /comments/23 요청을 서버로 전송하면,
[{"commentNo":100,,,,},{"commentNo":99,,,,},{"commentNo":98,,,,}]와 같은 JSON 객체가 상세보기 페이지에 전달된다.

다음 자바스크립트 함수를 상세보기 페이지에 추가한다.

function displayComments() {
  var url = '/comments/' + ${articleNo};
  $.getJSON(url, function (data) {
    $('#all-comments').empty();
    $.each(data, function (i, item) {
      var creation = new Date(item.regdate);
      var comments = '<div class="comments">'
        + '<span class="writer">' + item.name + '</span>'
        + '<span class="date">' + creation.toLocaleString() + '</span>';
      if (item.editable === true) {
        comments = comments
          + '<span class="modify-del">'
          + '<a href="#" class="comment-modify-link">' 
          + '수정' + '</a> | '
          + '<a href="#" class="comment-delete-link" title="' + item.commentNo + '">' 
          + '삭제' + '</a>'
          + '</span>';
        }
        comments = comments
          + '<div class="comment-memo">' + item.memo + '</div>'
          + '<form class="comment-form" action="/comments/' + ${articleNo } + '/' 
          + item.commentNo + '" method="put" style="display: none;">'
          + '<div style="text-align: right;">'
          + '<a href="#" class="comment-modify-submit-link">' 
          + '전송' 
          + '</a> | <a href="#" class="comment-modify-cancel-link">' 
          + '취소' + '</a>'
          + '</div>'
          + '<div>'
          + '<textarea class="comment-textarea" name="memo" rows="7" cols="50">' 
          + item.memo + '</textarea>'
          + '</div>'
          + '</form>'
          + '</div>';
        $('#all-comments').append(comments);
        console.log(item);
    });
  });
}

페이지가 처음 로드될 때
새 댓글을 작성했을 때
댓글이 수정될 때
댓글이 삭제될 때
displayComments() 함수가 실행돼야 한다.

댓글 관련 HTML 코드
<sf:form id="addCommentForm" action="/comments/${articleNo }" method="post" style="margin: 10px 0;">
    <div id="addComment">
        <textarea id="addComment-ta" name="memo" rows="7" cols="50"></textarea>
    </div>
    <div style="text-align: right;">
        <input type="submit" value="댓글쓰기" />
    </div>
</sf:form>
<!--  comments begin -->
<div id="all-comments">
</div>
<!--  comments end -->

<sf:form .. 처럼 쓰려면, 상세보기 페이지에 다음 선언이 필요하다.
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="sf" %>

div id="all-comments" 콘텐츠에 댓글 목록이 갱신된다.

아래를 참고해 페이지가 로드될 때와 댓글이 생성됐을 때 댓글 목록이 갱신되게 한다.

$(document).ready(function () {
  displayComments();
  
  //..omit..    
  
  $("#addCommentForm").submit(function (event) {
    event.preventDefault();
    var $form = $(this);
    var memo = $('#addComment-ta').val();
    memo = $.trim(memo);
    if (memo.length === 0) {
      $('#addComment-ta').val('');
      return false;
    }
    var dataToBeSent = $form.serialize();
    var url = $form.attr("action");
    var posting = $.post(url, dataToBeSent);
    posting.done(function () {
      displayComments();
      $('#addComment-ta').val('');
    });
  });    
});

displayComments() 함수가 실행될 때마다, div id="all-comments" 콘텐츠에 동적으로 댓글 HTML 코드가 생성된다. 동적으로 생성되는 댓글 HTML 코드에 수정과 삭제 링크가 있다면 DOM 프로그래밍으로 각각의 핸들러를 등록한다. --페이지가 로드된 후 동적으로 생성되는 HTML 코드에 DOM 프로그래밍은 $(document).on() 함수에서 해야 한다--

$(document).on('click', '#all-comments', function (e) {
  if ($(e.target).is('.comment-modify-link')) {
    e.preventDefault();
    var $form = $(e.target).parent().parent().find('.comment-form');
    var $div = $(e.target).parent().parent().find('.comment-memo');

    if ($form.is(':hidden') === true) {
      $form.show();
      $div.hide();
    } else {
      $form.hide();
      $div.show();
    }
  } else if ($(e.target).is('.comment-modify-cancel-link')) {
    e.preventDefault();
    var $form = $(e.target).parent().parent().parent().find('.comment-form');
    var $div = $(e.target).parent().parent().parent().find('.comment-memo');

    if ($form.is(':hidden') === true) {
      $form.show();
      $div.hide();
    } else {
      $form.hide();
      $div.show();
    }
  } else if ($(e.target).is('.comment-modify-submit-link')) {
    e.preventDefault();
    var $form = $(e.target).parent().parent().parent().find('.comment-form');
    var $textarea = $(e.target).parent().parent().find('.comment-textarea');
    var memo = $textarea.val();
    $('#modifyCommentForm input[name*=memo]').val(memo);
    var dataToBeSent = $('#modifyCommentForm').serialize();
    var url = $form.attr("action");
    $.ajax({
      url: url,
      type: 'PUT',
      data: dataToBeSent,
      success: function () {
        displayComments();
      },
      error: function () {
        alert('error!');
      }
    });
  } else if ($(e.target).is('.comment-delete-link')) {
    e.preventDefault();
    var chk = confirm('정말로 삭제하겠습니까?');
    if (chk === false) {
      return;
    }
    var $commentNo = $(e.target).attr('title');
    var url = $('#deleteCommentForm').attr('action');
    url += $commentNo;
    var dataToBeSent = $('#deleteCommentForm').serialize();
    $.ajax({
      url: url,
      type: 'POST',
      data: dataToBeSent,
      success: function () {
        displayComments();
      },
      error:function(request,status,error){
        console.log("code:"
          + request.status
          + "\n" + "message:" 
          + request.responseText 
          + "\n" + "error:"
          + error);
      }
    });
  }
});

댓글을 댓글을 삭제할 때 DELETE 요청이 아니라 모두 POST 요청인 것에 주목하라. --이유는 뒤에 다룬다-- 여기까지 작성하면 상세보기 페이지에서 댓글을 볼 수 있다.

새글

다음 새 댓글 쓰기 메소드를 댓글 컨트롤러에 추가한다.

@PostMapping("{articleNo}")
public void addComment(@PathVariable Integer articleNo, 
      String memo, Principal principal) {
    
  Comment comment = new Comment();
  comment.setMemo(memo);
  comment.setArticleNo(articleNo);
  comment.setEmail(principal.getName());

  boardService.addComment(comment);
}

이젠 상세보기 페이지에서 댓글을 추가할 수 있다.

수정

다음 수정 메소드를 댓글 컨트롤러에 추가한다.

@PutMapping("{articleNo}/{commentNo}")
public void updateComment(@PathVariable Integer articleNo, 
      @PathVariable Integer commentNo, String memo, Principal principal) {
      
  Comment comment = boardService.getComment(commentNo);
  comment.setMemo(memo);
  boardService.modifyComment(comment);
}

이젠 댓글을 수정할 수 있다.

삭제

다음 삭제 메소드를 댓글 컨트롤러에 추가한다.

@DeleteMapping("{articleNo}/{commentNo}") 
public void deleteComment(@PathVariable Integer articleNo, @PathVariable Integer commentNo) {
  Comment comment = boardService.getComment(commentNo);
  boardService.removeComment(comment);
}

스프링 MVC와 스프링 시큐리티를 사용하는 환경에서 DELETE 요청은 Request method 'DELETE' not supported 에러를 발생시킨다. 스프링 MVC만 사용하는 환경에선 DELETE 요청이 정상 동작한다.

스프링은 POST 요청을 PUT이나 DELETE 요청으로 매핑할 수 있다. 사실 이 기능은 브라우저가 PUT이나 DELETE 요청을 지원하지 않을 때 사용한다. 댓글 삭제를 위해 이 기능을 사용해 보자. --이때 댓글 수정 역시 수정해야 한다--

web.xml에 다음 필터를 추가한다.

<filter>
  <filter-name>httpMethodFilter</filter-name>
  <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>httpMethodFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

추가한 필터는 _method 파라미터 값을 판단하여, 요청을 핸들러에 전달한다.

상세보기 페이지에서 댓글 수정과 삭제 폼을 다음과 같이 수정한다.

<sf:form id="modifyCommentForm" method="put">
  <input type="hidden" name="memo" />
</sf:form>
<sf:form id="deleteCommentForm" action="/comments/${articleNo }/" method="delete">
</sf:form>

댓글 수정과 관련된 ajax 전송 코드에서 type을 PUT 에서 POST로 수정한다.

  } else if ($(e.target).is('.comment-modify-submit-link')) {
    e.preventDefault();
    var $form = $(e.target).parent().parent().parent().find('.comment-form');
    var $textarea = $(e.target).parent().parent().find('.comment-textarea');
    var memo = $textarea.val();
    $('#modifyCommentForm input[name*=memo]').val(memo);
    var dataToBeSent = $('#modifyCommentForm').serialize();
    var url = $form.attr("action");
    $.ajax({
      url: url,
      type: 'POST',
      data: dataToBeSent,
      success: function () {
        displayComments();
      },
      error: function () {
        alert('error!');
      }
    });
  } else if ($(e.target).is('.comment-delete-link')) {
관련 글