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カラム)