野球リーグスコア管理システムの開発

2019-01-13に作成

image
野球リーグスコア管理システムに関する進捗です。

[PR]参加者募集しています!

使っている技術など

  • NodeJS
  • ReactJS
  • netlify
  • MySQL
  • materializecss
  • react-bootstrap
  • react-bootstrap-table-next

旧システムについてはこちらの記事をご覧ください。

残りタスクリスト

trello

所有者限定モードのためこのボードには投稿できません ボードとは?

獲得タイトルに順位を表示するようにしました。

獲得タイトルとは

年度ごとに、打率、HR、打点、安打数、防御率、勝利数、セーブ、奪三振数の各タイトルの保持状況を表したもの。

今回実装したUI

テーマ:楽しめる人を増やす
元々の実装では1位(タイトル獲得者)しか表示されず、

  • 2位だったとかそういう情報がわからない
  • 参加しているのに表示されない

では一部の人しか面白くないと思ったので実装を変えることにしました。1-3位はメダル、10位以内入賞は順位を表示するよう変更しました。

image

実装裏話

タイトル集計バッチの実装

集計処理はリプレース前の旧システムで保持していたのですが、
今回の仕様変更に伴いNodeJSで新たに実装しました。

DBの破壊的変更

変更前はevent_typeというカラムを持って拡張性を考慮していましたが、逆にデータが取り出し辛いという欠点がありました。
また、1位の情報のみ保持しており、各タイトル10位までの情報が保持できないという欠点もありました。(定義が煩雑になる)

変更後はいつ誰が 何のタイトルで何位だったかを明確に視認できるようになり、データの可読性が上がりました。

変更前の定義

create table title_holder(
    player_id int(3) not null,
    season_id int(3) not null,
    event_type int(2) not null,
    value double not null,
    lock_flg int(1) not null,
    foreign key (player_id) references player(id),
    foreign key (season_id) references season(id),
    primary key(player_id,season_id,event_type)
);

変更後の定義

create table title_holder(
    player_id int(3) not null,
    season_id int(3) not null,
    average_rank int(2),
    homerun_rank int(2),
    rbi_rank int(2),
    hit_rank int(2),
    era_rank int(2),
    win_rank int(2),
    save_rank int(2),
    strikeout_rank int(2),
    lock_flg int(1) not null,
    foreign key (player_id) references player(id),
    foreign key (season_id) references league(id),
    primary key(player_id,season_id)
);

元々のUI

2/10ごろ完成したもの。
image

最後に

私の成績はそんなに上位ではないのですが、
最高で銅メダル(3位)取れていたのでちょっとうれしくなりました。

5/23進捗

おすすめサービスコンポーネントを作りました

image

実装

あまり難しいことはしてないのでまるっと公開してしまいますが。

  • array: おすすめサービスのインデックスをもつ配列
  • dispArray: 乱数生成した結果を格納する配列
  • num: 0からarray.lengthまでの数字から乱数を生成した結果

おすすめサービスの候補が仮に20個あったとして、
1. arrayにその20件の情報を格納します。
2. 0-19までの乱数を発生させ、該当するインデックスをdispArrayに格納し、arrayから削除します。
3. 残ったarrayに対して再度乱数を発生させます。

あとはReactのJSXを組み立てるだけですね。


export class RecommendServices extends React.Component { render() { if (store.getState().recommend === undefined) { let array = []; let dispArray = []; for (let i = 0; i < recommendConfig.length; i++) { array.push(i); } for (let j = 0; j < 3; j++) { let num = Math.floor(Math.random() * array.length); if(array[num] !== undefined){ dispArray.push(array[num]); } array.splice(num, 1); } console.log("dispArray", dispArray); store.dispatch(setRecommend(dispArray)); console.log(store.getState()); } let dispElm = []; for (let i = 0; i < store.getState().recommend.length; i++) { dispElm.push( <div className="card"> <div className="card-title"> <a href={recommendConfig[store.getState().recommend[i]].href}> <img src={recommendConfig[store.getState().recommend[i]].img} width={200}/> <br/> <div> {recommendConfig[store.getState().recommend[i]].title} &nbsp;<i className="fas fa-external-link-alt 2x" style={{ fontSize: 14 }}/> </div> </a> </div> <div style={{ fontSize: 12 }}> {recommendConfig[store.getState().recommend[i]].comment} </div> </div> ); } console.log(dispElm) return ( <div>{dispElm}</div> ); } }

recommendConfig

export const recommendConfig = [
  {
    title:'Crieit',
    href:'https://crieit.net/boards/baseball-score-management',
    img:'https://storage.googleapis.com/deviita/upload_images/a920719ad1329bd0abcef3505eda1be85c198e94430c7.png',
    comment:'本システムの開発の進捗を掲載させていただいています。'
  },
  {....}
]

5/22 進捗

レスポンシブ(2カラム)構成にしました。

ただしトップページだけー!
image
メニューをカード表示にしたら意外ときれいだったのですが、
右上にスライドメニューも配置しているのでどうしようかと思っていたのですが、モバイルファーストの1カラムから2カラム構成へ変更しました。

おすすめ個人開発サービスを掲載

image
お世話になっている個人開発者の方々に勝手に恩返しをします。
掲載してまずいようでしたら私まで苦情を...。

5/21 進捗

ピックアップゲーム機能

image
ランダムで試合動画を表示する仕様でしたが、動画を見ていると「表示している動画と試合結果を連動したい」と思って、

  • 乱数生成して試合IDを指定し、動画を表示
  • 指定した試合IDでAPIから試合データを取得
componentDidMount(){
    let videos = []
    for(let key in youtubeConfig){
      videos.push(key)
    }
    let idx = Math.floor(Math.random()* (videos.length + 1))
    fetchPickUpGame(videos[idx])
    this.setState({
      game_id: videos[idx]
    })
    console.log(videos[idx],this.state.game_id)
  }

若干回りくどい実装のような気もします...。

試合結果詳細に試合動画を表示するようにしました。

image

最近の試合結果にチーム名を表示するようにしました。

image

元々SQLでチーム名を取得する仕様にはなっていなかったところを修正しました。久々にAPI側に手を入れました...。

SELECT 
    g.game_id,
    g.game_date,
    g.game_number,
    g.first_team,
    g.last_team,
    g.first_run,
    g.last_run,
    g.league_id,
    t1.short_name as first_team_name,
    t2.short_name as last_team_name
from game g
inner join league l
on g.league_id = l.id
inner join team t1
on g.first_team = t1.team_id
inner join team t2
on g.last_team = t2.team_id
where g.game_date>=? and g.game_date<=?
order by g.game_date desc

5/20 スタイル修正

トップページのスタイルの修正

ちょこちょこスタイルを直しました。

直近の試合結果

image
ちょっと後回しにしていた部分なのですが、スコアがデフォルトの青字リンクになっていたのを、
- 勝者を強調する
- フォントサイズ調整
- アイコンも15->20px

という形で修正しました。

メニューを2カラム化

image
メニューを2カラムにしてフォントサイズを調整しました。
フッタにもメニューがあるのでどうしようかと悩み中...。

最後に

自分一人で作っていると妥協してしまう点があるので、
誰かに見てもらって意見をもらうということはいいことですね!

トップページ刷新(その2)

内容的には昨日の進捗の続きです。

チーム順位サマリ

image
試合結果の右側のスペースが空いていたのもあり、
今シーズンのチーム上位3チームの成績を表示するようにしました。
テーブルコンポーネントは恒例のreact-bootstrap-table-nextです。

スタッツサマリの部門分け

image
打者部門・投手部門で見出しのスタイルを変えるようにしました。
茶色の見出しは内野の土の色、白い見出しはベースの色をイメージしています。
また、あくまでスタッツのサマリなので、詳細に誘導するようリンクを作成しました。
データの都合上、シーズンが変わると「最新のスタッツ」のシーズンIDが変わるので、設定に

export const CURRENT_SEASON_ID = 41

と定数を設定してやり、「最新のスタッツ」のAPIを呼び出す際に定数を参照できるようにしました。

componentDidMount(){
     fetchSeasonResult(CURRENT_SEASON_ID)
  }

フッタにサイトマップ実装

image
ハンバーガーメニューは一応用意しているのですが、
「気づきにくいかも」という指摘もいただいており、フッタにサイトマップを実装しました。

作っているサービス

https://jcblscore-react.netlify.com/
ぜひ感想やご意見・フィードバックをお寄せください。

トップページ刷新

最新のスタッツへの直接アクセスが多い当サービスですが、トップページのコンテンツを充実させて刷新しました。

最近の試合結果リスト

image
直近の3試合を表示して試合結果詳細への導線をつけました。

API側変更

トップページにアクセスした際に「最新のスタッツ」APIを呼び出す仕様に変更し、APIで返す情報を増やす対応を行いました。

ピックアップ動画

youtubeに上げている試合動画をランダムで表示するように実装しました。

フロント側実装

試合IDとyoutubeの動画IDとの紐づけはソース上でこのようなプロパティを記述して直接更新しています。

export const youtubeConfig=
{
  //17-11-25
  "248":"1vBgiQhGW3E",
  "249":"hnF0JpGWzrk",
  "250":"QucROoxDudg",

一旦全てのキーを取得して配列に格納した後、Math.random()で乱数を生成して該当する配列のインデックスに格納されている動画を表示します。

スタッツサマリー

スタッツでTOP10を表示している項目を上位3人に絞って表示しています。
display:flexを駆使したのに文字の改行で項目の表示にずれが...orz
全部tableだと味気ないと思ったのですが。
image

作っているサービス

https://jcblscore-react.netlify.com/
ぜひ感想やご意見・フィードバックをお寄せください。

5/6アップデート

試合結果詳細の実装

image
これまで合計スコアのみの表示だった試合結果を、内訳まで表示するページを実装しました。

スコアボード実装小話

野球のスコアボードは後攻だけ色々なパターンがあって面倒なのです。例えば、試合を3イニングで打ち切った場合、

  • 先攻の勝利
  • 後攻が3回裏の攻撃を行ってサヨナラ勝ち
  • 後攻が3回裏の攻撃を行わず勝ち
    この3パターンが必要となります。
    今回は、3,4,5回打ち切りのパターンを用意しました。
    あまり4回打ち切りはないのですが。

Reactを使って書いています。スコアボードをコンポーネント化したので、データをpropsで渡しています。
3-5回の後攻のスコアボードの要素が状況によって変わるので、
要素を変えてDOMの配列にpushしています。

let last_team_score = []
        if(this.props.game.last_run > this.props.game.first_run){
            if(this.props.game.bottom_3rd === null){
                //3回終了、後攻X
                last_team_score.push(<td>X</td>)
                last_team_score.push(<td></td>)
                last_team_score.push(<td></td>)
                last_team_score.push(<td>{this.props.game.last_run}</td>)
            }else if(this.props.game.bottom_3rd !== null && this.props.game.top_4th === null){
                //3回終了、後攻サヨナラ勝ち
                last_team_score.push(<td>{this.props.game.bottom_3rd}x</td>)
                last_team_score.push(<td></td>)
                last_team_score.push(<td></td>)
                last_team_score.push(<td>{this.props.game.last_run}</td>)   
            }else if(this.props.game.bottom_4th === null){
                //4回終了、後攻X
                last_team_score.push(<td>{this.props.game.bottom_3rd}</td>)
                last_team_score.push(<td>X</td>)
                last_team_score.push(<td></td>)
                last_team_score.push(<td>{this.props.game.last_run}</td>)
            }else if(this.props.game.bottom_4th !== null && this.props.game.top_5th === null){
                //4回終了、後攻サヨナラ勝ち
                last_team_score.push(<td>{this.props.game.bottom_3rd}</td>)
                last_team_score.push(<td>{this.props.game.bottom_4th}x</td>)
                last_team_score.push(<td></td>)
                last_team_score.push(<td>{this.props.game.last_run}</td>) 
            }else if(this.props.game.bottom_5th === null){
                //5回終了、後攻X
                last_team_score.push(<td>{this.props.game.bottom_3rd}</td>)
                last_team_score.push(<td>{this.props.game.bottom_4th}</td>)
                last_team_score.push(<td>X</td>)
                last_team_score.push(<td>{this.props.game.last_run}</td>) 
            }else{
                //5回終了、後攻サヨナラ勝ち
                last_team_score.push(<td>{this.props.game.bottom_3rd}</td>)
                last_team_score.push(<td>{this.props.game.bottom_4th}</td>)
                last_team_score.push(<td>{this.props.game.bottom_5th}x</td>)
                last_team_score.push(<td>{this.props.game.last_run}</td>)    
            }
        }else{
            //先攻勝利
            last_team_score.push(<td>{this.props.game.bottom_3rd}</td>)
            last_team_score.push(<td>{this.props.game.bottom_4th}</td>)
            last_team_score.push(<td>{this.props.game.bottom_5th}</td>)
            last_team_score.push(<td>{this.props.game.last_run}</td>)
        }

5/2,3アップデート

月間打率推移グラフ

image

これまで蓄積したデータから毎年の月間打率を計算し、視覚化してみました。グラフのカラーセットを自動的に生成できると楽だなーと思うのですが。
この選手のグラフからは4,5月に好調を維持し、11月~2月あたりが不調という推測が成り立ちます。

QRコード表示機能

image

qrcode.reactというコンポーネントを使って実装しました。

背景

野球リーグの公式戦を開催する際に人数が不足していると、現地で試合に勧誘することがあります。よく口頭で「”カラーボール野球”で検索してください」と伝えてはいるのですが、やはりその場面で画面を見せてQRコードを読み取ってもらえると楽なのでは、と閃いて作りました。

継続参加者成績の実装

継続参加者成績の実装

image

直近2年間継続して参加している参加者の成績一覧を表示します。
基本的には1試合ごとの打席の内訳を1レコードとしてDBに保存しているので、選手IDと試合に参加した 年(試合日付をyear関数でまとめたもの) でGROUPBYします。
そうすると、参加年数分レコードが取れるので、2レコード以上ある選手を取得すればokです。

UI側はチームページが似たような実装なのでそれを使います。
なるべくコンポーネントを共通化するようにはしていますがなかなか難しいですね。

メニュー修正

image
新しいページを1つ増やすと、

-トップメニュー
- サイドメニュー
- ルーティング
- API問い合わせ

割と修正箇所多かったです..。

普段和暦はあまり使いませんが令和(こういうときだけ)最初のコミット、キメました。

4/28,29アップデート

打者最終出場日の表示

「全期間通算成績」ページで打者の最終出場日を表示するように実装しました。
例として、「通算打率20位の中で最近出場していたのは誰かな?」
という使い方ができます。
image

チームページ投球成績追加

時間がなくて1か月ぐらい作りかけで放置していましたが、
チームごとの投手通算成績を見ることができるようになりました。
image

デザイン修正

配色をcoolorsを元に見直しました。

  • タブ色の見直し
  • 選手名などのサブタイトルを外野の芝のイメージ色に
  • スクロールバーを内野の土をイメージした若干明るめの色に
  • パンくずリストの追加[階層をたどりやすくなりました]
    image

全期間通算成績の機能強化

投手部門の成績の指標にWHIP・奪三振率・K/BBを表示してソートできるようにしました。

WHIP

WHIP = ( 与四球 + 被安打 ) / 投球回
投手がどれぐらい走者を出しているかという指標。
数値が小さい方が優秀です。打高投低のリーグでは1点台で優秀だと思います。
だいたい防御率の順位と相関があることがわかります。

image

奪三振率

奪三振率(5イニング換算)=奪三振数 ÷ 投球回 * 5
5回投げた場合、投手がどれぐらい三振を奪えるかという指標。
やはり速球派が上位に名を連ねます。7点台はバケモン(笑)

image

K/BB

K/BB=奪三振÷与四球
投手の制球力を示す指標。これまで指標として取り入れていなかったのですが、iotasさんに勧められたので取り入れてみました。
与四球が少なくて三振を取れる投手が上位にきますね。
奪三振率、WHIPと比べてみてみるとなるほど…と思えます。
image

最終出場日の表示と類似度計算修正

image

最終出場日を表示する

現在、野球リーグでは2005年からの打撃・投球成績をExcelで保存しており、そのうちシステムには2010年度から現在までの成績の入力が終わっています。
約10年分の通算成績を見る際に、誰がアクティブなプレーヤーかという判別をできるようにするために、最終出場日を表示する機能を実装しました。

類似度計算の修正

先週、選手間の「類似度」を実装しました。
その際x軸に長打率(SLG)、y軸にAB/K(三振のしにくさ)を採用しましたが、正確さを向上させるため、K/BB(選球眼)をz軸に採用して3次元でのユークリッド距離を求めるよう修正しました。

3/15進捗

直近の試合結果で引分が表示できないバグ修正

image

チームメンバー打撃成績に詳細項目追加

image

3/13進捗

チームページにチーム通算成績表示を追加

image

あまり進捗が芳しくないですが、昼休みに少しだけ進めました。

  • 通算勝敗タブを新たに追加。
  • 通算成績表示