0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ポートフォリオ構築の振り返り(第11回:グループ機能を作る)

Posted at

はじめに

これまで個人での投稿(Item・ItemPost)を作ってきましたが、今回は グループ機能 を追加して「チームで投稿や管理ができる仕組み」を実装します。

たとえばこんなイメージです👇

  • ユーザーがグループを作成
  • グループにメンバーを招待
  • グループ内で投稿を共有できる

「コミュニティ的に使えるアプリ」にしたいときによく見る構造です。

これまでの記事はこちら👇
ポートフォリオ構築の振り返り(第1回:プロジェクト概要と設計)
ポートフォリオ構築の振り返り(第2回:Railsアプリ立ち上げ〜トップページ表示)
ポートフォリオ構築の振り返り(第3回:Deviseでログイン機能を実装)
ポートフォリオ構築の振り返り(第4回:ヘッダーの作成とログイン機能の実装)
ポートフォリオ構築の振り返り(第5回:投稿機能と画像投稿フォームの作成)
ポートフォリオ構築の振り返り(第6回:投稿機能の作成)
ポートフォリオ構築の振り返り(第7回:ユーザーカラム追加)
ポートフォリオ構築の振り返り(第8回:部分テンプレートを使ったマイページ作成)
ポートフォリオ構築の振り返り(第9回:itemのカード表示と共通化)
ポートフォリオ構築の振り返り(第10回:親子関係/リレーションを作る)


今回の流れ

  1. Groupモデル / GroupMemberモデル の作成

    • グループそのもの
    • メンバー管理の中間テーブル
  2. GroupsController / GroupMembersController の実装

    • グループ作成・編集・削除
    • メンバー招待・管理
  3. ビューの作成

    • グループ詳細ページにプロフィールと投稿一覧を表示
    • 新規投稿ボタンやカードを並べる
  4. 最後にまとめと用語説明


実装内容

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_userUser モデルと関連付け → 誰がグループを作ったかわかる
  • 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 と書くとページ切り替えができる。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?