게시판 - 댓글 기능 만들기 2 (수정, 삭제)

소스코드

이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실행할 수 있습니다.

이 게시물의 소스코드는 게시판 만들기(고급) / 게시판 - 댓글 기능 만들기 1 (쓰기, 보기)에서 이어집니다.

board.git 을 clone 한 적이 있는 경우: 터미널에서 해당 폴더로 이동 후 아래 명령어들을 붙여넣기합니다. 폴더 내 모든 코드가 이 게시물의 코드로 교체됩니다. 이를 원치 않으시면 이 방법을 선택하지 마세요.

git reset --hard
git pull
git reset --hard 8f4241b
git reset --soft edd77a9
npm install
atom .

board.git 을 clone 한 적이 없는 경우: 터미널에서 코드를 다운 받을 폴더로 이동한 후 아래 명령어들을 붙여넣기하여 board.git 을 clone 합니다.

git clone https://github.com/a-mean-blogger/board.git
cd board
git reset --hard 8f4241b
git reset --soft edd77a9
npm install
atom .

- Github에서 소스코드 보기: https://github.com/a-mean-blogger/board/tree/8f4241b7875d5da52a23b57500b67ba9b121bfcf


댓글 수정(update), 삭제(destroy) 기능을 추가해 봅시다.

삭제는 지금까지 해왔던 대로 하면 되고, 수정 기능은 조금 복잡합니다. 댓글(comment)의 보기(index/show) 및 생성(new) view과 마찬가지로 comment의 수정(edit) view 역시 post의 show 페이지에 작성되어야 하고, 여기에다 댓글은 여러개가 달리기 때문에 모든 댓글이 각각의 edit view를 가져야 하기 때문이죠. 댓글을 수정하기 위해 '댓글 수정'을 눌렀는데 댓글을 수정하는 다른 페이지로 이동하는 것은 너무 불편하잖아요.

그래서 아래와 같은 형식으로 댓글 수정 form을 만들겁니다.

물론 위처럼 모든 댓글의 edit form을 항상 사용자한테 보여주는 것도 좋은 사용자 경험이 아니기 때문에, jQuery, CSS를 사용해서 기본적으로 모든 수정 form들을 숨기고, 'edit'을 누르는 경우에만 해당 form이 보여지도록 할 예정입니다.

또한 댓글 생성에 사용되는 form와 댓글 수정에 사용되는 form이 거의 동일하기 때문에 하나의 form을 만들어 partial로 분리해 봅시다.

폴더 구조

코드 - js

// routes/comment.js

...

// create ...

// update // 2
router.put('/:id', util.isLoggedin, checkPermission, checkPostId, function(req, res){
  var post = res.locals.post;

  req.body.updatedAt = Date.now();
  Comment.findOneAndUpdate({_id:req.params.id}, req.body, {runValidators:true}, function(err, comment){
    if(err){
      req.flash('commentForm', { _id: req.params.id, form:req.body });
      req.flash('commentError', { _id: req.params.id, errors:util.parseError(err) });
    }
    return res.redirect('/posts/'+post._id+res.locals.getPostQueryString());
  });
});

// destroy // 3
router.delete('/:id', util.isLoggedin, checkPermission, checkPostId, function(req, res){
  var post = res.locals.post;

  Comment.findOne({_id:req.params.id}, function(err, comment){
    if(err) return res.json(err);

    // save updated comment
    comment.isDeleted = true;
    comment.save(function(err, comment){
      if(err) return res.json(err);

      return res.redirect('/posts/'+post._id+res.locals.getPostQueryString());
    });
  });
});

module.exports = router;

// private functions
function checkPermission(req, res, next){ // 1
  Comment.findOne({_id:req.params.id}, function(err, comment){
    if(err) return res.json(err);
    if(comment.author != req.user.id) return util.noPermission(req, res);

    next();
  });
}

function checkPostId( ...

1. post, user와 마찬가지로 작성자만이 댓글을 삭제할 수 있게 checkPermission middleware를 만들었습니다.

2. comment의 update route은 post와 거의 동일하지만, 에러가 있는 경우 commentForm, commentError flash에 comment의 id를 전달해 주는 것이 다릅니다. comment는 post와 다르게 하나의 페이지에 여러가지 edit form이 존재하기 때문에 정확히 어느 form에서 에러가 왔는지를 나타내 주기 위해서입니다.

3. 이전글에서 설명했던 것처럼, 댓글을 완전히 삭제해버리면 삭제된 댓글의 대댓글들이 고아가 되어버리기 때문에, 실제로 댓글을 삭제하지 않고 isDeletedtrue로 바꾸는 일만 합니다.

코드 - ejs

댓글의 update form은 모든 댓글당 하나씩 생성됩니다. 만약 댓글이 10개이면, 해당 웹페이지는 10개의 댓글 수정 form이 존재합니다. 하지만 이걸 다 보여주면 웹페이지가 지저분해지기 때문에 css class와 jQuery를 통해서 필요없는 form은 숨겨주고, 필요한 댓글 수정 form만 화면에 표시되게 됩니다.

실제 코드를 살펴보기 전에, 댓글이 3개인 가상의 코드를 살펴봅시다.

<!-- 참고용 html 코드-->

<div id="comment-111" class=""> <!-- 첫번째 댓글 -->
  <div class="comment-show">
    <!-- comment-show 코드 ... -->
    <a onclick="$('#comment-111').addClass('comment-edit-enabled')">Edit</a>
  </div>
  <div class="comment-edit">
    <!-- comment-edit 코드 ... -->
    <a onclick="$('#comment-111').removeClass('comment-edit-enabled')">Cancel</a>
  </div>
</div>

<div id="comment-222" class=""> <!-- 두번째 댓글 -->
  <div class="comment-show">
    <!-- comment-show 코드 ... -->
    <a onclick="$('#comment-222').addClass('comment-edit-enabled')">Edit</a>
  </div>
  <div class="comment-edit">
    <!-- comment-edit 코드 ... -->
    <a onclick="$('#comment-222').removeClass('comment-edit-enabled')">Cancel</a>
  </div>
</div>

<div id="comment-333" class=""> <!-- 세번째 댓글 -->
  <div class="comment-show">
    <!-- comment-show 코드 ... -->
    <a onclick="$('#comment-333').addClass('comment-edit-enabled')">Edit</a>
  </div>
  <div class="comment-edit">
    <!-- comment-edit 코드 ... -->
    <a onclick="$('#comment-333').removeClass('comment-edit-enabled')">Cancel</a>
  </div>
</div>
/* 참고용 css 코드*/

.comment-edit,
.comment-edit-enabled .comment-show {
  display:none;
}

.comment-edit-enabled .comment-edit {
  display:inherit;
}

client의 브라우저에는 위와같이 3개의 반복되는 html element가 전달되고 id field에 comment-commentId의 형태로 실제 DB상의 comment ID를 가집니다. jQuery를 통해 각 html element에 css class를 더하거나 뺄 수 있으며, css를 통해 comment-show, comment-edit이 보이거나 숨겨지게 됩니다.

처음 화면이 생성되면 css에 의해 모든 comment-show를 가지는 div들은 보이는 상태이고, comment-edit를 가지는 div들은 숨겨진 상태(display: none)가 됩니다.

comment-show안의 edit을 누르게 되면, 해당 댓글 div에 comment-show class가 추가되고, css에서 설정한 것 처럼 이제 comment-edit를 가지는 div들은 보이는 상태이고, comment-show를 가지는 div들은 숨겨진 상태(display: none)가 됩니다.

comment-edit안의 cancel을 누르게 되면, 해당 댓글 div에 comment-show class가 제거되고, 다시 처음의 상태로 돌아가게 됩니다.

이제 실제 코드를 살펴봅시다.

<!-- views/posts/show.ejs -->
      ...

      <h4 class="mt-3">Comments</h4>

      <% if(comments.length){ %>
        <div class="mt-3 border-bottom">
          <% comments.forEach(function(comment) { %>
            <div class="border-top pt-1 pb-1">
   <!-- 4 --> <% if(comment.isDeleted){ %>
                <div class="text-muted p-2">(Deleted Commnet)</div>
              <% } else { %>
              <div class="row">
                <div class="col-3 col-md-2 col-lg-1 pl-4"><%= comment.author.username %></div>
       <!-- 1 --> <div id="comment-<%= comment._id %>" class="col-9 col-md-10 col-lg-11 <%= (commentError._id == comment._id)?'comment-edit-enabled':'' %>">
                  <div class="comment-show">
                    <div class="comment-text mb-3"><%= comment.text %></div>
                    <small class="d-block">
                      (Created: <span data-date-time="<%= comment.createdAt %>"></span>
           <!-- 5 --> <% if(comment.updatedAt){ %>
                          | Updated: <span data-date-time="<%= comment.updatedAt %>"></span>
                      <% } %>
                      )
                    </small>
                    <% if(isAuthenticated && comment.author && currentUser.id == comment.author.id){ %>
                      <small class="d-block">
             <!-- 2 --> <a href="javascript:void(0)" onclick="$('.comment-reply-enabled').removeClass('comment-reply-enabled'); $('.comment-edit-enabled').removeClass('comment-edit-enabled'); $('#comment-<%= comment._id %>').addClass('comment-edit-enabled')">Edit</a>
                        |
             <!-- 6 --> <form action="/comments/<%= comment._id %>?postId=<%= post._id %>&_method=delete<%= getPostQueryString(true) %>" method="post" class="d-inline">
                          <a href="javascript:void(0)" onclick="confirm('Do you want to delete this?')?this.parentElement.submit():null;">Delete</a>
                        </form>
                      </small>
                    <% } %>
                  </div>
       <!-- 3 --> <div class="comment-edit">
                    <%- include('./partials/comment-form', {
                      actionUrl:'/comments/' + comment._id + '?postId=' + post._id + '&_method=put',
                      comment: comment,
                      commentForm: commentForm,
                      commentError: commentError,
                    }); %>
                  </div>
                </div>
              </div>
              <% } %>
            </div>
          <% }) %>
        </div>
      <% } %>

      <div class="mt-3"> <!-- 7 -->
        <%- include('./partials/comment-form', {
          actionUrl:'/comments?postId=' + post._id,
          comment: {},
          commentForm: commentForm,
          commentError: commentError,
        }); %>
      </div>

    </div>
  </body>
</html>

1. 참고용 html 코드에서 본 id field입니다. id="comment-<%= comment._id %>"를 써서 댓글의 id를 넣습니다. 다음으로 <%= (commentError._id == comment._id)?'comment-edit-enabled':'' %>도 봅시다. comment의 update route에서 에러가 오는 경우에 commentError._id는 에러를 가진 id를 값을 가지므로 해당 댓글의 comment-edit-enabled class를 활성화하여 페이지 로딩 후에 댓글 수정 form이 바로 보이도록 해줍니다.

2. 참고용 html 코드에서 본 edit 버튼(실제로는 문자열)입니다.. 이 문구를 누르게 되면 comment-edit-enabled class가 댓글 element에 추가됩니다.

3. 실제 edit form은 partial로 분리되었습니다. 이 form은 뒤에서 살펴보겠습니다. 이렇게 <%- include('view_코드_위치', 데이터 %>의 형태로 하위 view에 데이터를 전달할 수 있습니다.

4. 댓글이 삭제된 경우를 처리하는 코드입니다.

5. 댓글이 수정된 경우, 수정된 시간을 표시하는 코드입니다.

6. 댓글 삭제 버튼(실제로는 문자열)입니다.

7. 이전 강의에서 생선한 댓글 생성 form이 있던 자리인데, 댓글 수정 form과 거의 유사하기 때문에 partial로 통합하였습니다.

다음으로 partial로 분리된 comment form view 코드를 살펴봅시다.

<!-- views/posts/partials/comment-form.ejs -->

<%/* <!-- 1 -->
 actionUrl
 comment
 commentForm
 commentError
*/%>

<form action="<%= actionUrl %><%= getPostQueryString(true) %>" method="post">
  <fieldset <%= !isAuthenticated?'disabled':'' %>>
    <div class="row">
      <div class="col-8"> <!-- 2 -->
        <textarea name="text" rows="2" class="form-control <%= (commentError._id == comment._id && commentError.errors.text)?'is-invalid':'' %>"><%= (commentForm._id == comment._id)?commentForm.form.text:comment.text %></textarea>
      </div>
      <div class="col-4">
        <% if(comment._id){ %>
          <button type="submit" class="btn btn-primary h-100 mr-2 pl-4 pr-4">Edit</button>
<!-- 3 --><a href="javascript:void(0)" onclick="$('.comment-<%= comment._id %>').removeClass('comment-edit-enabled')">Cancel</a>
        <% } else { %>
          <button type="submit" class="btn btn-primary h-100 mr-2">Add Comment</button>
        <% } %>
      </div>
    </div>
    <% if(commentError._id == comment._id && commentError.errors.text){ %>
      <span class="invalid-feedback d-block"><%= commentError.errors.text.message %></span>
    <% } %>
  </fieldset>
</form>

1. 해당 partial view에 전달 될 수 있는 parameter들을 comment로 남겼습니다. 각각의 parameter들을 살펴봅시다.

  • actionUrl: 이 form은 comment의 수정과 생성 모두에 사용될 수 있기 때문에 각각의 용도에 따라 알맞은 action url이 전달되어져야 합니다.
  • comment: 댓글의 수정의 경우 원 댓글의 정보가 전달됩니다.
  • commentForm: 에러가 있는 경우 commentForm flash의 정보가 전달됩니다.
  • commentError: 에러가 있는 경우 commentError flash의 정보가 전달됩니다.

2. textarea의 is-invalid class를 생성하는 조건이 변경되었습니다. 변경 전과 후를 비교하여 살펴봅시다.

// 변경 전
class="form-control <%= (commentError.errors.text)?'is-invalid':'' %>"
// 변경 후
class="form-control <%= (commentError._id == comment._id && commentError.errors.text)?'is-invalid':'' %>"

이 comment-form partial은 한페이지에 여러번 반복이 되는데, 각각의 form은 다른 comment를 가지지만, 모두 같은 commentForm, commentError를 전달받습니다. comment._id은 댓글 생성의 경우 _id의 값이 없고, 댓글 수정의 경우 _id에 현재 댓글의 id가 들어있습니다. 마찬가지로 commentForm._id, commentError._id도 댓글 생성의 경우 id의 값이 없고, 댓글 수정의 경우 _id에 현재 댓글의 id가 들어있습니다. 그러므로 commentForm._id, commentError._idcomment._id과 값을 비교하면 어느 form에 해당 데이터를 사용해야 하는지를 알 수 있습니다.

3. 위 참고용 html에서 본 cancel 코드입니다.

코드 - css

/* publish/css/master.css */

...

.comment-edit,
.comment-edit-enabled .comment-show {
  display:none;
}

.comment-edit-enabled .comment-edit {
  display:inherit;
}

실행 결과

각각 댓글에 'edit'과 'delete'이 생겼습니다. 'edit'을 눌러봅시다.

내용을 수정한 후 Edit 버튼을 누릅니다.

정상적으로 댓글이 수정되는지를 확인합니다. 다음으로 에러 메세지 확인을 위해 'edit'을 다시 누르고 빈 댓글을 전송해 봅시다.

정확한 위치에 에러메세지가 표시되는지를 확인합니다.

마지막으로 댓글 삭제 기능을 테스트해봅시다.

댓글이 삭제되면 댓글이 삭제되었다는 문구를 표시합니다.

마치며..

코드가 좀 복잡한데.. 잘 이해할 수 있으셨는지 모르겠습니다. 잘 이해가 안되면 제가 설명이 부족한 탓이니 댓글 남겨주세요 ㅠㅠ 피드백이 절실합니다. 그리고.. 다음 글은 대댓글 기능인데.. 대댓글 기능은 이 글 보다 더 어렵습니다.

댓글

강성모 2020.03.31
<!-- views/posts/partials/comment-form.ejs --> 설명 란에 오타가 있어 댓글드립니다.! commentForm: 에러가 있는 경우 commentForm flash의 정보다 전달됩니다. commentError: 에러가 있는 경우 commentError flash의 정보다 전달됩니다. 위의 부분입니다!
잘보고 있습니다. 감사합니다.
I
Ian H 2020.03.31
@강성모,
안녕하세요! 제보해주셔서 감사합니다😅 해당 부분은 수정되었습니다. 
J
Jake Lyu 2020.04.29
  <!-- 5 --> <% if(comment.updatedAt){ %>                           | Updated: <span data-date-time="<%= coSSmment.updatedAt %>"></span>                       <% } %>
coSSment -->>> comment.updatedAt   ss 누르신거 같아요  clone하면 없는데 캡쳐화면상에 있습니다.
감사합니다.
J
Jake Lyu 2020.04.29
 <!-- 3 --> <div class="comment-edit">                     <%- include('partials/comment-form', {
partials 이부분 절대경로로 표시됐는데 잘못 표기된거 맞나요? ./partials인거 같은데 확인 부탁드립니다.
I
Ian H 2020.04.29
@Jake Lyu,
안녕하세요. 이렇게 강의 코드를 검수해 주시니 좋네요 ㅋㅋ 게시판 강의는 코드가 여러번 수정되면서 강의 본문의 코드는 오류가 좀 있는 편입니다 죄송합니다 ㅠㅠ github에 있는 코드는 이상이 있다면 코드 실행시에 티가 나니깐 고치기가 쉬운데 강의 본문에 있는 코드는 그렇지 않아서 많이 놓치는 편이예요. 이렇게 알려주시면 저에게 됩니다. 감사합니다!
댓글쓰기

이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기

UP