はじめに
これまで個人での投稿(Item・ItemPost)を作ってきましたが、今回は グループ機能 を追加して「チームで投稿や管理ができる仕組み」を実装します。
たとえばこんなイメージです👇
- ユーザーがグループを作成
- グループにメンバーを招待
- グループ内で投稿を共有できる
「コミュニティ的に使えるアプリ」にしたいときによく見る構造です。
これまでの記事はこちら👇
ポートフォリオ構築の振り返り(第1回:プロジェクト概要と設計)
ポートフォリオ構築の振り返り(第2回:Railsアプリ立ち上げ〜トップページ表示)
ポートフォリオ構築の振り返り(第3回:Deviseでログイン機能を実装)
ポートフォリオ構築の振り返り(第4回:ヘッダーの作成とログイン機能の実装)
ポートフォリオ構築の振り返り(第5回:投稿機能と画像投稿フォームの作成)
ポートフォリオ構築の振り返り(第6回:投稿機能の作成)
ポートフォリオ構築の振り返り(第7回:ユーザーカラム追加)
ポートフォリオ構築の振り返り(第8回:部分テンプレートを使ったマイページ作成)
ポートフォリオ構築の振り返り(第9回:itemのカード表示と共通化)
ポートフォリオ構築の振り返り(第10回:親子関係/リレーションを作る)
今回の流れ
-
Groupモデル / GroupMemberモデル の作成
- グループそのもの
- メンバー管理の中間テーブル
-
GroupsController / GroupMembersController の実装
- グループ作成・編集・削除
- メンバー招待・管理
-
ビューの作成
- グループ詳細ページにプロフィールと投稿一覧を表示
- 新規投稿ボタンやカードを並べる
-
最後にまとめと用語説明
実装内容
1. Groupモデル
グループ本体。オーナー(作成者)やメンバー、投稿(items)とつながります。
class Group < ApplicationRecord
belongs_to :owner_user, class_name: 'User', foreign_key: 'owner_id'
has_one_attached :image
validates :name, presence: true, length: { maximum: 100 }
validates :description, presence: true, length: { maximum: 500 }
validates :image, presence: true
has_many :group_members, dependent: :destroy
has_many :users, through: :group_members
has_many :items, dependent: :destroy
enum status: { active: 0, owner_delete: 1, admin_delete: 2 }
end
💡 ポイント
-
owner_userをUserモデルと関連付け → 誰がグループを作ったかわかる -
has_many :users, through: :group_members→ 中間テーブル経由で多対多の関係を管理
2. GroupMemberモデル
中間テーブルで「誰がどのグループに所属しているか」を管理します。
class GroupMember < ApplicationRecord
belongs_to :user
belongs_to :group
enum exit_reason: {
voluntary: 0, # 自主的に脱退
forced: 1 # 管理者による強制脱退
}
end
💡 ポイント
- 今回は
exit_reasonをenumで管理 → 将来的にログを残せる
3. GroupsController
グループ作成・表示・編集・削除を担当。
class Public::GroupsController < ApplicationController
before_action :ensure_correct_user, only: [:edit, :update, :destroy]
def new
@group = Group.new
end
def create
@group = Group.new(group_params)
@group.owner_id = current_user.id
if @group.save
# 作成者自身をメンバーとして登録
@group.group_members.create(user_id: current_user.id)
redirect_to group_path(@group)
else
render :new
end
end
def show
@group = Group.active.find_by(id: params[:id])
if @group.nil?
redirect_to items_path, alert: "存在しないグループです"
return
end
items = @group.items.includes(:user, :group)
item_posts = ItemPost.includes(:user, item: [:user, :group])
.where(item_id: items.pluck(:id))
# Item と ItemPost をまとめて一覧に渡す
@cards = Kaminari.paginate_array(items + item_posts).page(params[:page]).per(15)
end
private
def group_params
params.require(:group).permit(:name, :description, :image)
end
def ensure_correct_user
@group = Group.find(params[:id])
redirect_to items_path unless @group.owner_id == current_user.id
end
end
4. GroupMembersController
メンバー管理。メールアドレスで招待できるようにします。
class Public::GroupMembersController < ApplicationController
before_action :set_group
def index
@group_members = @group.group_members
.joins(:user)
.includes(:user)
.where(users: { status: User.statuses[:active] })
end
def create
user = User.find_by(email: params[:email])
if user && !@group.users.include?(user)
@group.group_members.create(user: user)
flash[:notice] = "メンバーを招待しました"
else
flash[:alert] = "ユーザーが存在しないか、すでにメンバーです"
end
redirect_to group_path(@group)
end
private
def set_group
@group = Group.find(params[:group_id])
end
end
5. ビュー(groups/show.html.erb)
グループの詳細ページ。プロフィール・投稿一覧・新規作成ボタンを並べます。
<div class="container">
<div class="row d-flex flex-wrap mt-3">
<!-- グループプロフィール -->
<div class="col-12 col-md-4 col-lg-3">
<%= render 'group_profile', group: @group %>
</div>
<!-- 投稿一覧 -->
<div class="col-12 col-md-8 col-lg-9 mb-4">
<div class="row">
<h4>グループ投稿一覧</h4>
<%= render partial: 'public/items/filter_bar', locals: { filter_url: group_path(@group) } %>
</div>
<div class="row">
<!-- 新規作成ボタン -->
<div class="col-12 col-sm-6 col-md-4 col-lg-3 mb-4">
<%= link_to new_group_item_path(@group), class: "text-decoration-none" do %>
<div class="card h-100 position-relative rounded-4 d-flex align-items-center justify-content-center" style="min-height: 320px;">
<i class="fa-solid fa-plus fa-2x"></i>
</div>
<% end %>
</div>
<!-- 共通カードパーシャル -->
<% @cards.each do |card| %>
<%= render "public/items/card", card: card %>
<% end %>
</div>
<div class="d-flex justify-content-center my-4">
<%= paginate @cards %>
</div>
</div>
</div>
</div>
💡 ポイント
-
group_profileをパーシャル化 → 他のページでも再利用可能 - 投稿は
items/_card.html.erbを流用 → 個人とグループでデザイン統一
まとめ
- Group / GroupMember モデル で「グループ」と「メンバー管理」を実現
- GroupsController / GroupMembersController でグループの作成・編集・招待を実装
- ビュー ではプロフィール表示+投稿一覧をカードで表示
これで「グループで投稿や管理ができる」仕組みができました!
次は グループプロフィールのパーシャル や メンバー権限管理(管理者・一般メンバー) を入れるとさらに実用的になります。
次回はコメント機能といいね機能の実装を振り返ります!
用語説明(初心者向け)
-
中間テーブル
多対多(ユーザーとグループの関係)のときに使うテーブル。
今回はGroupMemberがそれにあたる。 -
enum
数値をシンボルで扱えるRailsの機能。
status: { active: 0, owner_delete: 1 }のように書ける。 -
パーシャル
ビューの共通部分を切り出したファイル。render '...'で呼び出す。 -
Kaminari
ページネーション(ページ分割)用のgem。
paginate @cardsと書くとページ切り替えができる。