1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Rails】バドミントンダブルス練習試合 管理アプリ

Last updated at Posted at 2025-08-28

1. 概要

  • 管理者ログインのみ/閲覧は誰でもOK
  • 対戦自動作成:
    • 男女別(男子→女子を交互に。2コート問題の修正込み)
    • MIX(性別フリー、参加回数が均等になるよう自動割当)
  • 公開↔非公開の切替ボタンあり(見た目は素のボタン)

2. 新規プロジェクト作成

コマンドプロンプト
rails new badminton_doubles
cd badminton_doubles

3. Devise 導入(管理者のみ)

コマンドプロンプト
bundle add devise
rails g devise:install
rails g devise Admin
rails db:migrate

4. ルーティング

config/routes.rb
Rails.application.routes.draw do
  devise_for :admins, path: "admin", skip: [:registrations]

  namespace :admin do
    root "events#index"
    resources :members
    resources :events do
      post  :auto_schedule, on: :member
      patch :publish,       on: :member
      patch :unpublish,     on: :member
    end
  end

  # 公開側(誰でも閲覧可能)
  resources :events, only: [:index, :show]
  root "events#index"
end

5. モデル & マイグレーション

コマンドプロンプト
rails g model Member name:string grade:integer skill_level:integer gender:integer active:boolean
rails g model Event  title:string date:date court_count:integer status:integer
rails g model Round  event:references index:integer status:integer
rails g model Match  round:references court_number:integer pair1_member1:references pair1_member2:references pair2_member1:references pair2_member2:references

修正するマイグレーション:すべて書き換えてください!

db/migrate/xxxxxx_create_members.rb
class CreateMembers < ActiveRecord::Migration[7.2]
  def change
    create_table :members do |t|
      t.string  :name,        null: false
      t.integer :grade,       null: false
      t.integer :skill_level, null: false, default: 0
      t.integer :gender,      null: false, default: 0
      t.boolean :active,      null: false, default: true
      t.timestamps
    end
    add_index :members, :active
    add_index :members, :gender
    add_index :members, :skill_level
  end
end
db/migrate/xxxxxx_create_events.rb
class CreateEvents < ActiveRecord::Migration[7.2]
  def change
    create_table :events do |t|
      t.string  :title,       null: false
      t.date    :date,        null: false
      t.integer :court_count, null: false, default: 2
      t.integer :status,      null: false, default: 0 # draft
      t.timestamps
    end
    add_index :events, :date
    add_index :events, :status
  end
end
db/migrate/xxxxxx_create_rounds.rb
class CreateRounds < ActiveRecord::Migration[7.2]
  def change
    create_table :rounds do |t|
      t.references :event, null: false, foreign_key: true
      t.integer :index,    null: false
      t.integer :status,   null: false, default: 0
      t.timestamps
    end
    add_index :rounds, [:event_id, :index], unique: true
  end
end
db/migrate/xxxxxx_create_matches.rb
class CreateMatches < ActiveRecord::Migration[7.2]
  def change
    create_table :matches do |t|
      t.references :round, null: false, foreign_key: true
      t.integer :court_number, null: false
      t.references :pair1_member1, null: false, foreign_key: { to_table: :members }
      t.references :pair1_member2, null: false, foreign_key: { to_table: :members }
      t.references :pair2_member1, null: false, foreign_key: { to_table: :members }
      t.references :pair2_member2, null: false, foreign_key: { to_table: :members }
      t.timestamps
    end
    add_index :matches, [:round_id, :court_number], unique: true
  end
end
コマンドプロンプト
rails db:migrate

6. モデル定義

:registerable を削除する!

削除前:

app/models/admin.rb
class Admin < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end

削除後:

app/models/admin.rb
class Admin < ApplicationRecord
  devise :database_authenticatable,
         :recoverable, :rememberable, :validatable
end

以降はすべてコピペ!

app/models/member.rb
class Member < ApplicationRecord
  enum skill_level: { beginner: 0, middle: 1, advanced: 2 }
  enum gender:      { male: 0, female: 1 }

  scope :active, -> { where(active: true) }

  GRADE_OPTIONS = [
    ["1年", 1], ["2年", 2], ["3年", 3], ["4年", 4],
    ["M1", 5], ["M2", 6]
  ].freeze

  GRADE_LABELS = {
    1 => "1年", 2 => "2年", 3 => "3年", 4 => "4年",
    5 => "M1", 6 => "M2"
  }.freeze

  validates :name, :skill_level, :gender, presence: true
  validates :grade, presence: true, inclusion: { in: 1..6 }

  def skill_label
    I18n.t("enums.member.skill_level.#{skill_level}")
  end

  def gender_label
    I18n.t("enums.member.gender.#{gender}")
  end

  def grade_label
    GRADE_LABELS[grade] || grade.to_s
  end
end
app/models/event.rb
class Event < ApplicationRecord
  has_many :rounds, dependent: :destroy
  enum status: { draft: 0, published: 1, archived: 2 }, _prefix: true

  validates :title, :date, :court_count, presence: true
  validates :court_count, numericality: { greater_than: 0 }
end
app/models/round.rb
class Round < ApplicationRecord
  belongs_to :event
  has_many :matches, dependent: :destroy

  enum status: { scheduled: 0, finished: 1 }, _prefix: true
  validates :index, presence: true, numericality: { greater_than: 0 }
end
app/models/match.rb
class Match < ApplicationRecord
  belongs_to :round
  belongs_to :pair1_member1, class_name: "Member"
  belongs_to :pair1_member2, class_name: "Member"
  belongs_to :pair2_member1, class_name: "Member"
  belongs_to :pair2_member2, class_name: "Member"

  validates :court_number, presence: true
  validate :members_are_all_distinct

  def members
    [pair1_member1, pair1_member2, pair2_member1, pair2_member2]
  end

  private
  def members_are_all_distinct
    ids = members.map(&:id)
    errors.add(:base, "同じメンバーを同一試合に重複して割り当てられません") if ids.uniq.size != ids.size
  end
end

7. ロケール(日本語ラベル)

config/localesフォルダに ja.yml ファイルを作成する!

config/locales/ja.yml
ja:
  enums:
    member:
      skill_level:
        beginner: 初心者
        middle: 中級者
        advanced: 上級者
      gender:
        male: 男子
        female: 女子
config/application.rb
module BadmintonDoubles
  class Application < Rails::Application
    config.load_defaults 7.2
    config.i18n.default_locale = :ja # この行を追加
  end
end

8. サービス:対戦自動作成(2コート修正 & MIX均等化)

1. appフォルダに services フォルダを作成する!

2. app/servicesフォルダに scheduling フォルダを作成する!

3. app/services/schedulingフォルダに auto_schedule.rb ファイルを作成する!

app/services/scheduling/auto_schedule.rb
require "set"

module Scheduling
  class Result < Struct.new(:success?, :error_message); end

  class AutoSchedule
    def initialize(event, target_rounds: nil, mode: "split")
      @event         = event
      @courts        = event.court_count.to_i
      @members       = Member.where(active: true).to_a
      @target_rounds = target_rounds
      @mode          = mode
    end

    def call
      @played_counts = Hash.new(0)
      return Result.new(false, "メンバー不足(4名以上必要)") if @members.size < 4

      ActiveRecord::Base.transaction do
        @event.rounds.destroy_all
        case @mode
        when "split" then schedule_split
        when "mixed" then schedule_mixed
        else raise ArgumentError, "未知のモード: #{@mode}"
        end
      end
      Result.new(true, nil)
    rescue => e
      Result.new(false, e.message)
    end

    private

    #========================
    # 男女別(男子→女子を同ラウンド内で詰めて配置)
    # ※ 1試合=2ペア なので、必要ペア数は「コート数×2」
    #========================
    def schedule_split
      males   = @members.select(&:male?)
      females = @members.select(&:female?)
      raise "男子または女子の人数が足りません(各4名以上)" if males.size < 4 || females.size < 4

      rounds        = @target_rounds || 1
      male_courts   = (@courts / 2.0).ceil
      female_courts = @courts - male_courts

      (1..rounds).each do |idx|
        round    = @event.rounds.create!(index: idx)
        used_ids = Set.new

        # 男子
        male_pairs_needed = male_courts * 2
        male_pairs        = build_pairs_rotating(males, male_pairs_needed, used_ids)
        male_made         = create_matches_from_pairs(round, 1, male_courts, male_pairs)

        # 女子(残りコートで)
        remain_courts        = @courts - male_made
        female_matches_need  = [female_courts, remain_courts].min
        female_pairs_needed  = female_matches_need * 2
        female_pairs         = build_pairs_rotating(females, female_pairs_needed, used_ids)
        create_matches_from_pairs(round, 1 + male_made, female_matches_need, female_pairs)
      end
    end

    #========================
    # MIX:性別フリー。毎試合「出場回数の少ない4名」を取り、
    #        レベル差の合計が最小になる2ペアで組ませる(ヘルパー無し)
    #========================
    def schedule_mixed
      rounds = @target_rounds || 1

      (1..rounds).each do |idx|
        round = @event.rounds.create!(index: idx)

        1.upto(@courts) do |court|
          pool = @members.sort_by { |m| [@played_counts[m.id], m.id] }
          break if pool.size < 4
          a, b, c, d = pool.first(4)

          candidates = [
            [[a, b], [c, d]],
            [[a, c], [b, d]],
            [[a, d], [b, c]]
          ]

          pair_score = ->(pair) do
            gap = (skill(pair[0]) - skill(pair[1])).abs
            gap <= 1 ? gap : 10 # 2差は重めに
          end

          p1, p2 = candidates.min_by { |x, y| pair_score.call(x) + pair_score.call(y) }
          create_match!(round, court, p1, p2)
        end
      end
    end

    #========================
    # 以下、同性ペア生成/公平ローテ/試合生成ユーティリティ
    #========================
    def create_matches_from_pairs(round, start_court, court_count, pairs)
      return 0 if court_count <= 0 || pairs.blank?
      list  = pairs.sort_by { |p| pair_avg_level(p) }.dup
      court = start_court
      made  = 0
      while made < court_count && list.size >= 2
        p1 = list.shift
        j  = list.each_with_index.min_by { |q, _i| (pair_avg_level(q) - pair_avg_level(p1)).abs }&.last
        break unless j
        p2 = list.delete_at(j)
        create_match!(round, court, p1, p2)
        court += 1
        made  += 1
      end
      made
    end

    def build_pairs_rotating(pool, need_pairs, used_ids)
      cand = pool.reject { |m| used_ids.include?(m.id) }
                 .sort_by { |m| [@played_counts[m.id], skill(m), m.id] }
      pairs = []
      while pairs.size < need_pairs && cand.size >= 2
        a = cand.shift
        b_idx = cand.each_with_index
                   .select { |x, _i| (skill(a) - skill(x)).abs <= 1 }
                   .min_by  { |x, _i| (skill(a) - skill(x)).abs }&.last
        b_idx ||= cand.each_with_index.min_by { |x, _i| (skill(a) - skill(x)).abs }&.last
        break unless b_idx
        b = cand.delete_at(b_idx)
        pairs << [a, b]
        used_ids << a.id << b.id
      end
      pairs
    end

    def pair_avg_level(pair)
      pair.sum { |mem| skill(mem) } / 2.0
    end

    def skill(m)
      case m.skill_level
      when "beginner" then 0
      when "middle"   then 1
      when "advanced" then 2
      else 0
      end
    end

    def create_match!(round, court_number, pair1, pair2)
      round.matches.create!(
        court_number:,
        pair1_member1: pair1[0], pair1_member2: pair1[1],
        pair2_member1: pair2[0], pair2_member2: pair2[1]
      )
      (pair1 + pair2).each { |m| @played_counts[m.id] += 1 }
    end
  end
end

9. コントローラー(管理 & 公開)

コマンドプロンプト
rails g controller admin/base
rails g controller admin/members
rails g controller admin/events
rails g controller events

9.1 管理ベース

app/controllers/admin/base_controller.rb
class Admin::BaseController < ApplicationController
  before_action :authenticate_admin!
  layout "admin"
end

9.2 メンバー CRUD(管理)

app/controllers/admin/members_controller.rb
class Admin::MembersController < Admin::BaseController
  before_action :set_member, only: %i[edit update destroy]

  def index
    @members = Member.order(:grade, :gender, :skill_level, :name)
  end

  def new
    @member = Member.new
  end

  def create
    @member = Member.new(member_params)
    if @member.save
      redirect_to admin_members_path, notice: "メンバーを追加しました"
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit; end

  def update
    if @member.update(member_params)
      redirect_to admin_members_path, notice: "メンバーを更新しました"
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @member.destroy
    redirect_to admin_members_path, notice: "メンバーを削除しました"
  end

  private
  def set_member
    @member = Member.find(params[:id])
  end

  def member_params
    params.require(:member).permit(:name, :grade, :skill_level, :gender, :active)
  end
end

9.3 イベント(管理)

app/controllers/admin/events_controller.rb
class Admin::EventsController < Admin::BaseController
  before_action :set_event, only: [:show, :edit, :update, :destroy, :publish, :unpublish, :auto_schedule]

  def index
    @events = Event.order(date: :desc)
  end

  def show
    counts = Hash.new(0)
    @event.rounds.each do |r|
      r.matches.each do |m|
        [m.pair1_member1, m.pair1_member2, m.pair2_member1, m.pair2_member2].each { |mem| counts[mem] += 1 }
      end
    end
    @participations = counts.to_a.sort_by { |(mem, c)| [-c, mem.name] }
  end

  def new
    @event = Event.new(date: Date.today, court_count: 2)
  end

  def create
    @event = Event.new(event_params)
    if @event.save
      redirect_to admin_event_path(@event), notice: "イベントを作成しました"
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit; end

  def update
    if @event.update(event_params)
      redirect_to admin_event_path(@event), notice: "イベントを更新しました"
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @event.destroy
    redirect_to admin_events_path, notice: "イベントを削除しました"
  end

  def publish
    if @event.update(status: :published)
      redirect_to admin_event_path(@event), notice: "イベントを公開しました"
    else
      redirect_to admin_event_path(@event), alert: "公開に失敗しました"
    end
  end

  def unpublish
    if @event.update(status: :draft)
      redirect_to admin_event_path(@event), notice: "イベントを非公開にしました"
    else
      redirect_to admin_event_path(@event), alert: "非公開に失敗しました"
    end
  end

  def auto_schedule
    rounds = params[:rounds].to_i
    rounds = nil if rounds <= 0
    mode   = params[:mode] || "split"

    result = ::Scheduling::AutoSchedule.new(@event, target_rounds: rounds, mode: mode).call
    if result.success?
      redirect_to admin_event_path(@event), notice: "対戦表を作成しました(#{@event.rounds.count}回戦)"
    else
      redirect_to admin_event_path(@event), alert: result.error_message
    end
  end

  private
  def set_event
    @event = Event.includes(rounds: :matches).find(params[:id])
  end

  def event_params
    params.require(:event).permit(:title, :date, :court_count, :status)
  end
end

adminフォルダ内の events コントローラーかどうかちゃんと確認してね!

9.4 公開側

app/controllers/events_controller.rb
class EventsController < ApplicationController
  def index
    @events = Event.status_published.order(date: :desc)
  end

  def show
    @event = Event.includes(rounds: :matches).find(params[:id])
  end
end

10. レイアウト(共通CSS / ヘッダーに「公開一覧」)

自分で app/views/layouts フォルダ内に admin.html.erb を作成する!

10.1 管理レイアウト

app/views/layouts/admin.html.erb
<!DOCTYPE html>
<html>
<head>
  <title>Admin - Badminton</title>
  <%= csrf_meta_tags %>
  <%= csp_meta_tag %>
  <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
  <%= javascript_importmap_tags %>
</head>
<body>
  <header class="site-header">
    <span class="brand">管理画面</span>
    <nav>
      <%= link_to "イベント", admin_events_path %>
      <%= link_to "メンバー", admin_members_path %>
      <%= link_to "公開一覧", events_path %>
    </nav>
    <div class="right">
      <%= button_to "ログアウト", destroy_admin_session_path,
          method: :delete, form: { style: "display:inline;" } %>
    </div>
  </header>

  <main class="page">
    <%= yield %>
  </main>
</body>
</html>

10.2 公開レイアウト

コマンドプロンプトapp/views/layouts/application.html.erb
# body内を書き換え
<body>
  <header class="site-header">
    <span class="brand">公開サイト</span>
    <nav>
      <%= link_to "公開一覧", events_path %>
      <%= link_to "管理者ログイン", new_admin_session_path %>
    </nav>
    <div class="right"></div>
  </header>

  <main class="page">
    <%= yield %>
  </main>
</body>

11. CSS を application.css に一本化(管理/公開共通)

app/assets/stylesheets/application.css
:root{
  --bg:#fff;--fg:#111;--muted:#6b7280;--accent:#6b46c1;--border:#cbd5e1;
  --header-bg:#222;--header-fg:#fff;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0;background:var(--bg);color:var(--fg);
  font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans JP","Hiragino Kaku Gothic ProN",Meiryo,sans-serif;
  line-height:1.6}
a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}

/* header */
.site-header{background:var(--header-bg);color:var(--header-fg);padding:10px 14px;display:flex;gap:16px;align-items:center}
.site-header .brand{font-weight:700;background:#fff1;padding:4px 8px;border-radius:6px}
.site-header nav a{color:var(--header-fg);margin-right:12px}
.site-header .right{margin-left:auto}

/* layout */
.page{padding:20px}
h1{font-size:34px;margin:8px 0 16px}
h2{font-size:22px;margin:24px 0 10px}

/* table */
.table{border-collapse:collapse;min-width:520px}
.table th,.table td{border:2px solid var(--border);padding:8px 10px;text-align:left;white-space:nowrap}
.table th{background:#f8fafc}

/* 2カラム(管理のイベント詳細) */
.layout-2col{display:flex;gap:16px;align-items:flex-start}
.layout-2col .col-left{flex:0 0 360px;max-width:420px;position:sticky;top:64px;max-height:calc(100vh - 80px);overflow:auto;border:1px solid var(--border);padding:8px;background:#fff;border-radius:6px}
.layout-2col .col-right{flex:1 1 auto;min-width:0}
@media(max-width:900px){
  .layout-2col{flex-direction:column}
  .layout-2col .col-left{position:static;max-height:none;width:100%}
}

12. ビュー

12.1 管理:メンバー

app/views/admin/members/index.html.erb
<h1>メンバー一覧</h1>
<p><%= link_to "新規メンバー", new_admin_member_path %></p>
<table class="table">
  <thead>
    <tr><th>名前</th><th>学年</th><th>性別</th><th>レベル</th><th>状態</th><th></th></tr>
  </thead>
  <tbody>
    <% @members.each do |m| %>
      <tr>
        <td><%= m.name %></td>
        <td><%= m.grade_label %></td>
        <td><%= m.gender_label %></td>
        <td><%= m.skill_label %></td>
        <td><%= m.active? ? '参加' : '休み' %></td>
        <td>
          <%= link_to '編集', edit_admin_member_path(m) %> |
          <%= link_to '削除', admin_member_path(m),
              data: { turbo_method: :delete, turbo_confirm: '削除しますか?' } %>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>
app/views/admin/members/_form.html.erb
<%= form_with model: [:admin, @member], local: true do |f| %>
  <div><%= f.label :name, '名前' %><br><%= f.text_field :name %></div>
  <div><%= f.label :grade, '学年' %><br>
    <%= f.select :grade, options_for_select(Member::GRADE_OPTIONS, @member.grade), include_blank: '選択してください' %></div>
  <div><%= f.label :gender, '性別' %><br>
    <%= f.select :gender, Member.genders.keys.map { |k| [I18n.t("enums.member.gender.#{k}"), k] } %></div>
  <div><%= f.label :skill_level, 'レベル' %><br>
    <%= f.select :skill_level, Member.skill_levels.keys.map { |k| [I18n.t("enums.member.skill_level.#{k}"), k] } %></div>
  <div><%= f.label :active, '参加する' %> <%= f.check_box :active %></div>
  <div style="margin-top:8px;"><%= f.submit %></div>
<% end %>
app/views/admin/members/new.html.erb
<h1>メンバー新規</h1>
<%= render 'form' %>
app/views/admin/members/edit.html.erb
<h1>メンバー編集</h1>
<%= render 'form' %>

12.2 管理:イベント一覧(詳細のみ)

app/views/admin/events/index.html.erb
<h1>イベント一覧</h1>
<p><%= link_to "新規イベント", new_admin_event_path %></p>
<table class="table">
  <thead>
    <tr><th>日付</th><th>タイトル</th><th>コート数</th><th>公開</th><th></th></tr>
  </thead>
  <tbody>
    <% @events.each do |e| %>
      <tr>
        <td><%= e.date %></td>
        <td><%= e.title %></td>
        <td><%= e.court_count %></td>
        <td><%= e.status_published? ? '✅' : '—' %></td>
        <td><%= link_to '詳細', admin_event_path(e) %></td>
      </tr>
    <% end %>
  </tbody>
</table>
app/views/admin/events/new.html.erb
<h1>イベント新規</h1>
<%= form_with model: [:admin, @event], local: true do |f| %>
  <div><%= f.label :title, 'タイトル' %><br><%= f.text_field :title %></div>
  <div><%= f.label :date, '日付' %><br><%= f.date_field :date %></div>
  <div><%= f.label :court_count, 'コート数' %><br><%= f.number_field :court_count, min: 1 %></div>
  <div style="margin-top:8px;"><%= f.submit %></div>
<% end %>
app/views/admin/events/edit.html.erb
<h1>イベント編集</h1>
<%= form_with model: [:admin, @event], local: true do |f| %>
  <div><%= f.label :title, 'タイトル' %><br><%= f.text_field :title %></div>
  <div><%= f.label :date, '日付' %><br><%= f.date_field :date %></div>
  <div><%= f.label :court_count, 'コート数' %><br><%= f.number_field :court_count, min: 1 %></div>
  <div style="margin-top:8px;"><%= f.submit "更新する" %></div>
<% end %>
app/views/admin/events/show.html.erb
<h1><%= @event.title %>(<%= @event.date %>)</h1>
<p>コート数<%= @event.court_count %></p>

<div style="display:flex; gap:16px; flex-wrap:wrap;">
  <%= form_with url: auto_schedule_admin_event_path(@event), method: :post, local: true do %>
    <label>何回戦?</label> <%= number_field_tag :rounds, 2, min: 1 %>
    <%= hidden_field_tag :mode, "split" %>
    <%= submit_tag "男女別で作成" %>
  <% end %>

  <%= form_with url: auto_schedule_admin_event_path(@event), method: :post, local: true do %>
    <label>何回戦?</label> <%= number_field_tag :rounds, 2, min: 1 %>
    <%= hidden_field_tag :mode, "mixed" %>
    <%= submit_tag "MIXで作成" %>
  <% end %>
</div>

<% if @event.status_draft? %>
  <p style="margin-top:8px;"><%= button_to "公開する", publish_admin_event_path(@event), method: :patch %></p>
<% else %>
  <p style="margin-top:8px; color:green;">このイベントは公開中です </p>
  <%= button_to "非公開に戻す", unpublish_admin_event_path(@event), method: :patch,
      data: { turbo_confirm: "本当に非公開にしますか?" } %>
<% end %>

<p style="margin-top:12px;">
  <%= link_to "イベントを編集", edit_admin_event_path(@event) %> |
  <%= button_to "イベントを削除", admin_event_path(@event),
        method: :delete, data: { turbo_confirm: "削除すると元に戻せません。削除しますか?" } %>
</p>

<div class="layout-2col">
  <!-- 参加回数 -->
  <aside class="col-left">
    <h2>参加回数このイベント</h2>
    <table class="table" style="min-width:320px;">
      <thead>
        <tr><th>名前</th><th>学年</th><th>性別</th><th>レベル</th><th>回数</th></tr>
      </thead>
      <tbody>
        <% @participations.each do |mem, cnt| %>
          <tr>
            <td><%= mem.name %></td>
            <td><%= mem.grade_label %></td>
            <td><%= mem.gender_label %></td>
            <td><%= mem.skill_label %></td>
            <td><%= cnt %></td>
          </tr>
        <% end %>
      </tbody>
    </table>
  </aside>

  <!-- 右:対戦表 -->
  <section class="col-right">
    <h2>対戦表</h2>
    <% @event.rounds.order(:index).each do |r| %>
      <% r.matches.order(:court_number).each do |m| %>
        <% gs = [m.pair1_member1.gender, m.pair1_member2.gender, m.pair2_member1.gender, m.pair2_member2.gender] %>
        <% label = gs.all?("male") ? "男子" : (gs.all?("female") ? "女子" : "MIX") %>
        <h3 style="margin-top:24px;"><%= r.index %> 回戦 <%= m.court_number %>コート <%= label %></h3>
        <table class="table" style="margin-bottom:12px; min-width:560px;">
          <thead><tr><th>ペア1</th><th>ペア2</th></tr></thead>
          <tbody>
            <tr>
              <td>
                <%= m.pair1_member1.name %>(<%= m.pair1_member1.grade_label %>・<%= m.pair1_member1.gender_label %><%= m.pair1_member1.skill_label %>)
                + <%= m.pair1_member2.name %>(<%= m.pair1_member2.grade_label %><%= m.pair1_member2.gender_label %>・<%= m.pair1_member2.skill_label %>)
              </td>
              <td>
                <%= m.pair2_member1.name %>(<%= m.pair2_member1.grade_label %>・<%= m.pair2_member1.gender_label %><%= m.pair2_member1.skill_label %>)
                + <%= m.pair2_member2.name %>(<%= m.pair2_member2.grade_label %><%= m.pair2_member2.gender_label %>・<%= m.pair2_member2.skill_label %>)
              </td>
            </tr>
          </tbody>
        </table>
      <% end %>
    <% end %>
  </section>
</div>

12.3 公開:イベント

app/views/events/index.html.erb
<h1>イベント一覧公開</h1>
<table class="table">
  <thead>
    <tr><th>日付</th><th>タイトル</th><th>コート数</th></tr>
  </thead>
  <tbody>
    <% @events.each do |e| %>
      <tr>
        <td><%= e.date %></td>
        <td><%= link_to e.title, event_path(e) %></td>
        <td><%= e.court_count %></td>
      </tr>
    <% end %>
  </tbody>
</table>
app/views/events/show.html.erb
<h1><%= @event.title %>(<%= @event.date %>)</h1>
<% @event.rounds.order(:index).each do |r| %>
  <% r.matches.order(:court_number).each do |m| %>
    <% gs = [m.pair1_member1.gender, m.pair1_member2.gender, m.pair2_member1.gender, m.pair2_member2.gender] %>
    <% label = gs.all?("male") ? "男子" : (gs.all?("female") ? "女子" : "MIX") %>
    <h3 style="margin-top:24px;"><%= r.index %> 回戦 <%= m.court_number %>コート <%= label %></h3>
    <table class="table" style="margin-bottom:12px;">
      <thead><tr><th>ペア1</th><th>ペア2</th></tr></thead>
      <tbody>
        <tr>
          <td>
            <%= m.pair1_member1.name %>(<%= m.pair1_member1.grade_label %>・<%= m.pair1_member1.gender_label %>) +
            <%= m.pair1_member2.name %>(<%= m.pair1_member2.grade_label %>・<%= m.pair1_member2.gender_label %>)
          </td>
          <td>
            <%= m.pair2_member1.name %>(<%= m.pair2_member1.grade_label %>・<%= m.pair2_member1.gender_label %> +
            <%= m.pair2_member2.name %>(<%= m.pair2_member2.grade_label %>・<%= m.pair2_member2.gender_label %>
          </td>
        </tr>
      </tbody>
    </table>
  <% end %>
<% end %>

13. Seed(管理者&ダミーメンバー例)

db/seeds.rb
Admin.destroy_all
Admin.create!(email: "admin@example.com", password: "password") # 複数追加

自分の登録したいメアドとパスワード編集してね!

複数赤運登録したい場合は二行目を追加したい分だけ増やしてね

コマンドプロンプト
rails db:seed

14. 使い方

  • 管理ログイン → メンバー登録(参加/休み切替可)
  • イベント新規(タイトル・日付・コート数)
  • イベント詳細で「何回戦?」→ 男女別で作成 もしくは MIXで作成
  • 男女別:男子→女子を同ラウンドで詰める(2コートでも必ず作成)
  • MIX:性別無視、参加回数が均等になるよう自動割当
  • 公開する(公開一覧に表示)/非公開に戻す(一覧から消える)
  • 管理詳細の左に参加回数、右に対戦表(2カラム)
1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?