posts 컨트롤러 변경

중첩 라우팅

RESTful URI로부터 bulletin_id 또는 postid 값을 받아 해당 bulletin 객체를 생성하거나 이 bulletin 객체의 posts 개체를 불러오기 위해서는 중첩 라우팅(nested routing) 기법을 사용하면 편리하다.

config/routes.rb 파일을 열어 아래와 같이 수정한다.

Rails.application.routes.draw do
  resources :bulletins do
    resources :posts
  end

  root 'welcome#index'
end

위와 같이 리소스 라우트를 중첩하면 아래와 같은 라우팅을 사용할 수 있게 된다. 이것을 콘솔에서 확인해 보자.

$ bin/rake routes
            Prefix Verb   URI Pattern                                      Controller#Action
    bulletin_posts GET    /bulletins/:bulletin_id/posts(.:format)          posts#index
                   POST   /bulletins/:bulletin_id/posts(.:format)          posts#create
 new_bulletin_post GET    /bulletins/:bulletin_id/posts/new(.:format)      posts#new
edit_bulletin_post GET    /bulletins/:bulletin_id/posts/:id/edit(.:format) posts#edit
     bulletin_post GET    /bulletins/:bulletin_id/posts/:id(.:format)      posts#show
                   PATCH  /bulletins/:bulletin_id/posts/:id(.:format)      posts#update
                   PUT    /bulletins/:bulletin_id/posts/:id(.:format)      posts#update
                   DELETE /bulletins/:bulletin_id/posts/:id(.:format)      posts#destroy
         bulletins GET    /bulletins(.:format)                             bulletins#index
                   POST   /bulletins(.:format)                             bulletins#create
      new_bulletin GET    /bulletins/new(.:format)                         bulletins#new
     edit_bulletin GET    /bulletins/:id/edit(.:format)                    bulletins#edit
          bulletin GET    /bulletins/:id(.:format)                         bulletins#show
                   PATCH  /bulletins/:id(.:format)                         bulletins#update
                   PUT    /bulletins/:id(.:format)                         bulletins#update
                   DELETE /bulletins/:id(.:format)                         bulletins#destroy
              root GET    /                                                welcome#index

위와 같은 라우팅 테이블에서 URI Pattern을 주목하자. 외부로부터 들어오는 요청이 이 테이블의 URI Pattern과 일치할 경우 매핑되는 컨트롤러의 액션이 호출된다. 이 때 URI Pattern 중 심볼에 매칭되는 부분은 params 해쉬의 키로 사용되어 해당 파라미터의 값을 불러올 수 있게 된다. 위의 예에서는 params[:bulletin_id] 키에 해당하는 파라미터 값을 불러와 액션에서 사용할 수 있게 된다.

특정 게시판의 게시물 목록을 불러오는 예를 들어 보자.

Prefix : bulletin_posts
Verb : GET
URI Pattern : /bulletins/:bulletin_id/posts(.:format)
Controller#Action : posts#index

Prefix 끝에 _path 또는 _url을 붙여 헬퍼메소드로 사용할 수 있는데, 뷰 템플릿에서 동적 URL을 사용할 수 있도록 해 주어 정적 URL을 일일이 입력할 필요가 없게 된다. 즉, 뷰 템플릿 파일에서 <%= bulletin_posts_path('공지사항') %>와 같이 사용하면 뷰 파일이 렌더링될 때 http://localhost:3000/bulletins/공지사항/posts으로 변환된다.

또한 이 URI Pattern에 매핑되는 해당 컨트롤러의 액션에서는 params[:bulletin_id] 해시키를 이용하여 '공지사항' 값을 얻을 수 있게 된다.

posts 컨트롤러의 변경

:bulletins:posts 리소스의 중첩 라우팅을 사용하기 위해서는 posts 컨트롤러도 수정해야 한다. 먼저 변경된 posts 컨트롤러 전체를 살펴보고 바뀐 부분을 분석해보자.

class PostsController < ApplicationController

  before_action :set_bulletin
  before_action :set_post, only: [:show, :edit, :update, :destroy]

  def index
    @posts = @bulletin.posts.all
  end

  def show
  end

  def new
    @post = @bulletin.posts.new
  end

  def edit
  end

  def create
    @post = @bulletin.posts.new(post_params)

    respond_to do |format|
      if @post.save
        format.html { redirect_to [@post.bulletin, @post], notice: 'Post was successfully created.' }
        format.json { render :show, status: :created, location: @post }
      else
        format.html { render :new }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

  def update
    respond_to do |format|
      if @post.update(post_params)
        format.html { redirect_to [@post.bulletin, @post], notice: 'Post was successfully updated.' }
        format.json { render :show, status: :ok, location: @post }
      else
        format.html { render :edit }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

  def destroy
    @post.destroy
    respond_to do |format|
      format.html { redirect_to bulletin_posts_url, notice: 'Post was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    def set_bulletin
      @bulletin = Bulletin.find(params[:bulletin_id])
    end

    def set_post
      @post = @bulletin.posts.find(params[:id])
    end

    def post_params
      params.require(:post).permit(:title, :content)
    end
end

먼저 private 메소드인 set_bulletin을 실행하는 before_action 필터가 지정되어 있다. 이 메소드는 posts 컨트롤러의 모든 액션이 실행되기 전에 수행될 것이다.(반면 set_post 메소드는 only 옵션에 의해 show, edit, update, destroy 액션이 실행되기 전에만 수행된다.)

class PostsController < ApplicationController

  before_action :set_bulletin
  before_action :set_post, only: [:show, :edit, :update, :destroy]

index 액션에서 인스턴스 변수 @bulletinposts 앞에 추가되었다. @bulletinset_bulletin 메소드에서 생성되는데 선택한 게시판(bulletin)에 대한 객체가 할당된다. 이렇게 해서 특정 게시판에 속하는 글을 모두 보여주거나(@bulletin.posts.all) 글을 저장할 때 해당 게시판에 포함되도록(@bulletin.posts.new) 할 수 있다.

  def index
    @posts = @bulletin.posts.all
  end

  def show
  end

  def new
    @post = @bulletin.posts.new
  end

  def edit
  end

새로운 글을 생성하는 create 액션에서도 게시판과 글의 종속 관계를 정의한다.(@bulletin.posts.new) 원래는 redirect_to @post로 객체를 다음 show 액션으로 리다이렉트했지만 게시판과의 종속 관계 때문에 리다이렉트 하는 과정에서 어떤 게시판에 속하는 post인지 알려줘야 하므로 redirect_to [@post.bulletin, @post]로 변경되었다. update 액션도 마찬가지다. [@post.bulletin, @post]와 같이 종속관계의 두 객체를 배열로 표시하는 것은 bulletin_post_path(@post.bulletin, @post) 또는 url_for([@post.bulletin, @post])의 축약형이다.

  def create
    @post = @bulletin.posts.new(post_params)

    respond_to do |format|
      if @post.save
        format.html { redirect_to [@post.bulletin, @post], notice: 'Post was successfully created.' }
        format.json { render :show, status: :created, location: @post }
      else
        format.html { render :new }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

  def update
    respond_to do |format|
      if @post.update(post_params)
        format.html { redirect_to [@post.bulletin, @post], notice: 'Post was successfully updated.' }
        format.json { render :show, status: :ok, location: @post }
      else
        format.html { render :edit }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

destroy 액션에서 글을 삭제하는 과정은 변함이 없다. Bulletin 모델과 Post 모델과의 관계는 일대다 관계로 글이 삭제된다고 해당 게시판에 영향을 미치지는 않기 때문이다. 반대로 게시판이 삭제되면 해당 게시판 내의 글이 모두 삭제되어야 하는데 이는 Bulletin 모델을 선언할 때 Post 모델과의 관계를 dependent: :destroy로 정의했기 때문에 자동으로 처리된다. 객체를 삭제한 다음 액션이 종료될 때 index 액션으로 리다이렉트 되는데 중첩 라우팅에 의해 index 액션의 경로 헬퍼 prefix가 posts에서 bulletin_posts로 바뀌었다. 따라서 리다이렉트 url도 bulletin_posts_url로 수정한다.

  def destroy
    @post.destroy
    respond_to do |format|
      format.html { redirect_to bulletin_posts_url, notice: 'Post was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

마지막으로 private 메소드가 남았다. private 메소드는 클래스 안에서만 호출할 수 있는데, before_action 필터에 의해 먼저 실행될 set_bulletin 메소드는 파라미터로 넘겨 받은 게시판 id를 통해 해당 객체를 @bulletin 인스턴스 변수에 할당한다. 이렇게 해서 posts 컨트롤러에서 각 액션을 처리할 때 해당 글이 속하는 게시판 정보를 함께 처리할 수 있게 된다.

  private
    def set_bulletin
      @bulletin = Bulletin.friendly.find(params[:bulletin_id])
    end

    def set_post
      @post = @bulletin.posts.find(params[:id])
    end

    def post_params
      params.require(:post).permit(:title, :content)
    end
end

Git소스 https://github.com/rorlakr/rcafe/tree/chapter_05_08