掲示板実装
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled

This commit is contained in:
2026-02-19 22:30:59 +09:00
parent f25fd6f802
commit bb9ec2df1d
38 changed files with 1711 additions and 13 deletions

61
app/models/board.rb Normal file
View File

@@ -0,0 +1,61 @@
class Board < ApplicationRecord
belongs_to :game
belongs_to :creator, class_name: "Participant",
foreign_key: "created_by_participant_id", optional: true
has_many :board_memberships, dependent: :destroy
has_many :active_memberships, -> { where(left_at: nil) },
class_name: "BoardMembership"
has_many :participants, through: :board_memberships
has_many :board_posts, dependent: :destroy
has_many :board_proposals, dependent: :destroy
enum :board_type, { global: "global", negotiation: "negotiation" }
validate :no_duplicate_member_combination, if: :negotiation?
# メンバーかどうか判定退出済みはfalse
def member?(participant)
return false unless participant
board_memberships.exists?(participant_id: participant.id, left_at: nil)
end
# 履歴モードかどうかゲーム終了後は常にtrue
def history_mode?
game.status == "finished"
end
# 指定した参加者の未読件数を取得
def unread_count_for(participant)
return 0 unless participant
membership = board_memberships.find_by(participant_id: participant.id)
# メンバーでない、または退出済みの場合は未読なしとするあるいは全件とするか要検討だが、一旦0
return 0 unless membership && membership.active?
last_id = membership.last_read_post_id || 0
board_posts.where("id > ?", last_id).count
end
private
def no_duplicate_member_combination
# 新規作成時のみチェック(更新時はメンバー変動があるため別途考慮が必要だが、今回は作成時のみ想定)
# ただしメンバー追加時にもチェックが必要になる可能性があるため、Boardモデルのバリデーションとして定義
# 比較対象のメンバーIDリストソート済み
current_member_ids = board_memberships.map(&:participant_id).sort
# 同じゲーム内の他の交渉用掲示板を検索
game.boards.negotiation.where.not(id: id).each do |other_board|
# 退出者を含めた全メンバー構成で比較するか、アクティブメンバーのみで比較するか
# 仕様書「同じメンバー構成の掲示板は作成できない」
# ここでは「現在参加しているメンバー」の構成が重複しないようにする
other_member_ids = other_board.board_memberships.where(left_at: nil).pluck(:participant_id).sort
if other_member_ids == current_member_ids
errors.add(:base, "同じメンバー構成の掲示板がすでに存在します")
return
end
end
end
end

View File

@@ -0,0 +1,21 @@
class BoardMembership < ApplicationRecord
belongs_to :board
belongs_to :participant
# 参加中かどうか(退出していない)
def active?
left_at.nil?
end
# 退出処理
def leave!
update!(left_at: Time.current)
end
# 既読位置を更新
def mark_read!(post_id)
# 既存の既読位置より新しい場合のみ更新(巻き戻り防止)
# ただし今回は単純に最新をセットする形で良い
update!(last_read_post_id: post_id)
end
end

26
app/models/board_post.rb Normal file
View File

@@ -0,0 +1,26 @@
class BoardPost < ApplicationRecord
belongs_to :board
belongs_to :participant
validates :body, presence: true
# 作成後にAction Cableで配信
after_create_commit :broadcast_to_board
private
def broadcast_to_board
# ボードごとのチャンネルに配信
ActionCable.server.broadcast(
"board_#{board_id}",
{
post_id: id,
participant_id: participant.id,
power: participant.power, # 国名
body: body,
phase: phase, # フェーズ情報
created_at: created_at.in_time_zone("Asia/Tokyo").strftime("%Y-%m-%d %H:%M")
}
)
end
end

View File

@@ -0,0 +1,9 @@
class BoardProposal < ApplicationRecord
belongs_to :board
belongs_to :proposer, class_name: "Participant",
foreign_key: "proposer_participant_id"
enum :status, { pending: "pending", accepted: "accepted", rejected: "rejected" }
validates :body, presence: true
end

View File

@@ -2,6 +2,8 @@ class Game < ApplicationRecord
has_many :turns, dependent: :destroy
has_many :participants, dependent: :destroy
has_many :users, through: :participants
has_many :boards, dependent: :destroy
has_one :latest_turn, -> { order(number: :desc) }, class_name: "Turn"
# パスワード保護
has_secure_password :password, validations: false
@@ -26,6 +28,9 @@ class Game < ApplicationRecord
# ターンスケジュールバリデーション
validate :validate_turn_schedule
# コールバック
after_create :create_global_board
# ヘルパーメソッド
def password_protected?
password_digest.present?
@@ -162,4 +167,8 @@ class Game < ApplicationRecord
errors.add(:turn_schedule, "は0〜23の数値をカンマ区切りで入力してください例: 0,18")
end
end
def create_global_board
boards.create!(board_type: "global", is_public: true)
end
end

View File

@@ -1,19 +1,23 @@
class Participant < ApplicationRecord
belongs_to :user
belongs_to :game
has_many :board_memberships, dependent: :destroy
has_many :boards, through: :board_memberships
has_many :board_posts, dependent: :destroy
# バリデーション
validates :user_id, uniqueness: {
validates :user_id, uniqueness: {
scope: :game_id,
message: "既にこのゲームに参加しています"
}
validates :power, uniqueness: {
validates :power, uniqueness: {
scope: :game_id,
message: "この国は既に選択されています"
}, allow_nil: true
validates :power, inclusion: {
validates :power, inclusion: {
in: %w[AUSTRIA ENGLAND FRANCE GERMANY ITALY RUSSIA TURKEY],
message: "無効な国です"
}, allow_nil: true