Post에 Comment 붙이기

이 프로젝트에서는 Post 모델에 대해서만 댓글기능을 추가하면 되기 때문에 굳이 Post 모델에 대한 Comment 모델의 관계선언을 polymorphic으로 할 필요가 없다.

우선 댓글을 달기 위해서는 로그인 상태여야 한다. 또한 :user, :author, :admin 권한을 가지면 댓글을 달 수 있다. 물론 본인이 작성한 댓글에 대해서만 삭제 권한을 가질 수 있고 댓글은 수정할 수 없도록 하자.

아래는 CommentAuthorizer 클래스의 코드를 보여주며, deletable_by 인스턴스 메소드를 추가한다. 이 메소드는 본인이 작성한 댓글만을 삭제할 수 있는 권한을 부여한다. 물론 :admin 권한을 가진 사용자는 모든 댓글을 삭제할 수 있다.

class CommentAuthorizer < ApplicationAuthorizer # :user, :author, :admin 권한이 있는 사용자만 댓글을 작성할 수 있음. def self.creatable_by?(user) user.has_role?(:admin) || user.has_role?(:author) || user.has_role?(:user) end # :admin 권한이 있거나 본인이 작성한 리소스에 대해서만 삭제할 수 있음. def deletable_by?(user) resource.user == user || user.has_role?(:admin) end end

이미 앞서 Comment 모델을 작성하였기 때문에 :body 속성을 필수항목으로 지정한다.

class Comment < ActiveRecord::Base resourcify include Authority::Abilities belongs_to :user belongs_to :post validates :body, presence: true end

Post 모델에는 Comment 모델에 대해서 has_many 메소드를 이용하여 관계선언한다.

... has_many :comments, dependent: :destroy ...

이제 app/controllers/comments_controller.rb 파일을 열고 createdestroy 액션을 작성한다.

class CommentsController < ApplicationController before_action :authenticate_user! before_action :set_post before_action :set_comment, only: :destroy def create authorize_action_for Comment @comment = @post.comments.new(comment_params) @comment.user = current_user @comment.save respond_to do | format | format.js end end def destroy authorize_action_for @comment @comment.destroy respond_to do | format | format.js end end private def set_post @post = Post.find(params[:post_id]) end def set_comment @comment = @post.comments.find(params[:id]) end def comment_params params.require(:comment).permit(:body) end end

여기서는 ajax로 댓글을 생성하고 삭제한다. 이유는 post 뷰 페이지에서 페이지의 이동없이 바로 댓글을 추가하고 삭제하기 위함이다.

app/views/posts/_post.html.erb 파일을 열고 댓글의 갯수만 표시하도록 하자. 이것은 posts#index 액션의 뷰 페이지에서 보여줄 파셜 템플릿 파일이다.

... <% if post.comments.count > 0 %> <div class='comments'> <%= icon('comment') %> <%= post.comments.count %> </div> <% end %> ...

이제 app/views/comments 디렉토리를 생성하고 _comments.html.erb 파일을 아래와 같이 작성한다.

<% comment = post.comments.new %> <div id="comments_<%=post.id%>" class='post_comments'> <h4>Comments <small>( <%= post.comments.count %> )</small></h4> <div id="list_of_comments_<%=post.id%>" class='list_of_comments'> <ul class='comments'> <%= render post.comments %> </ul> </div> <div id="form_of_comments_<%=post.id%>" class="comment_form"> <%= render partial: "comments/form", locals: { comment: comment } %> </div> </div>

실제로 댓글을 표시할 파셜 템플릿 파일을 작성하기 위해서 app/views/comments/_comment.html.erb 파일을 생성하고 아래와 같이 작성한다.

<% if comment.body.present? %> <li id="comment_<%=comment.id%>" class="comment"> <%= simple_format comment.body %> <div style='text-align:right;margin-top:0;border-top:1px solid #eaeaea;'> <small>- <%= comment.user.email %>, <%= time_ago_in_words(comment.created_at)%> ago</small> <% if user_signed_in? && current_user.can_delete?(comment) %> <%= link_to icon('trash'), [comment.post, comment], method: :delete, remote: true, data: { confirm: 'Are you sure?'} %> <% end %> </div> </li> <% end %>

여기시 주목할 것은 하단에 위치한 댓글 삭제용 링크다. link_to 메소드 옵션으로 remote: true를 지정한다. 이로써 삭제 링크를 클릭했을 때 HTTP DELETE 메소드를 이용하여 posts/:post_id/comments/:id URI로 ajax 요청을 하게 된다. 결과적으로 comments#destroy 액션이 동작하게 되고 destroy.js.erb을 렌더링하여 응답하게 된다.

위에서 댓글 입력을 위해서 사용할 Comment 모델의 폼 파셜 파일을 작성한다.

app/views/comments/_form.html.erb 파일을 생성하고 아래와 같이 작성한다.

<%= simple_form_for [comment.post, comment], remote: true do | f | %> <%= f.input :body, placeholder: 'Add a comment', label: false, input_html: { class: "form-control", rows: 5 } %> <div style='text-align:right;'> <%= f.button :submit, class: 'button tiny radius' %> </div> <% end %>

여기서 주목할 것은 simple_form_for 헬퍼 메소드 옵션으로 remote: true를 지정했다는 것이다. 이로써 HTTP POST 메소드를 이용하여 /posts/:post_id/comments URI를 ajax로 요청하게 되고 서버에서는 comments#create 액션이 호출된 후 결과로써 create.js.erb가 렌더링된 후 사용자의 브라우저로 보내지게 된다.

app/views/posts/show.html.erb 파일을 열고 하단에 아래와 같이 댓글 표시할 부분으로 파셜 뷰 템플릿을 렌더링하도록 작성한다.

<%= render partial: 'comments/comments', locals: { post: @post } %>

이상의 것을 정리하면, 댓글 생성할 때 create 액션 결과로 app/views/comments/create.js.erb 파일이 렌더링되어 클라이언트로 응답하고,

<% if @comment.errors.size == 0 %> $('<%=j render @comment %>').appendTo($("#list_of_comments_<%[email protected] %> ul")).hide().fadeIn('fast'); $("#form_of_comments_<%[email protected] %> form")[0].reset(); <% else %> alert("Please submit after commenting..."); <% end %>

댓글 삭제시는 destroy 액션의 응답으로 app/views/comments/destroy.js.erb 파일을 렌더링하여 클라이언트로 응답하게 된다.

$("#comment_<%[email protected] %>").slideUp('fast');

댓글 관련 CSS를 정리하여 app/assets/stylesheets/comments.scss 파일에 아래와 같이 추가하고,

ul.comments { li.comment { list-style: none; border-left:1em solid #eaeaea; padding-left:.5em; margin-bottom: 1em; p { margin-bottom: .3em; } } } .post_comments { margin-bottom:2em; }

이 파일을 application.scss 파일에 임포트한다.

... @import 'comments'; ...

_post.html.erb 파일에서 사용할 CSS를 수정하기 위해 app/assets/posts/posts.scss 파일을 열고 아래와 같이 수정한다.

... .comments { font-size: .8em; } .tags { margin-top: .3em; } .actions { margin-top:1em; } ...

app/views/posts/show.html.erb 파일을 열고 아래와 같이 수정한다.

<div class='post'> <div class='title'> <H3><%= @post.title %></H3> </div> <div class='author'> created by <%= @post.user.email %>, <%= time_ago_in_words(@post.created_at) %> ago </div> <div class='content'> <%= simple_format @post.content %> </div> <div class='category'> <%= icon('folder') + ' ' + @post.category.name %> </div> <div class='tags'> <%= icon_tags(@post.tag_list) %> </div> <% if user_signed_in? && current_user.can_update?(@post) %> <div class='published'> <%= published_icon @post.published %> </div> <% end %> </div> <hr> <div style='text-align:right;'> <%= link_to 'Edit', edit_post_path(@post), class: 'button tiny' %> <%= link_to 'Back', posts_path, class: 'button tiny' %> </div> <%= render partial: 'comments/comments', locals: { post: @post } %>

이상의 작업 내용을 요약하면, posts#index 액션으로 뷰 페이지를 볼 때는 댓글의 갯수만 보이도록 하고 posts#show 액션으로 댓글을 하나씩 볼 때는 댓글의 상세한 내용을 볼 수 있게 한다.

자 이제, 다음 장에서 검색 기능을 추가해 보자.

지금까지 작업한 내용을 로컬 저장소로 커밋한다.

$ git add . $ git commit -m "제09장 : Post에 Comment 붙이기" $ git tag "제09장"


소스보기 https://github.com/luciuschoi/foundblog_app/tree/제09장