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

2019-01-13に作成

image
野球リーグスコア管理システムキャップ野球情報局に関する進捗です。

使っている技術など

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

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

残りタスクリスト

trello

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

DB再設計(2) & ルーティング設計

DB論理設計

image
XmindでリレーションとかPKの表現っぽいものができますね。
やはりマインドマップツールだけあって構想を練る時には便利。
現行システムは14テーブルですが、ほとんど使っていないテーブルがいくつかあるのと、内容的に重複するテーブルは統合しようかと思っています。

ルーティング設計

image
画面が結構多いので画面遷移図というよりはURLをどうするかの構想を起こしています。いくつか機能面もまとめています。

使いやすいね、Xmind

ちょっと慣れるまでに時間がかかりましたが、慣れてくると自由度が高いソフトですね。
意外と早めに仕様が固まったので早めに着手していけそうです。

DB再設計始めました。

現在、閲覧のみに特化した野球リーグ管理システムを公開していますが、機能的に追加開発するところが段々なくなってきました。

  • 現在1リーグのみに特化したシステムを複数リーグで扱えるように
  • データベースへの登録・更新機能も公開する

以上2点を大きな軸としてサービスリリースに向けて徐々に動き始めました。複数リーグを扱う上でDBの再設計は不可避です。
ということで論理設計からやり直しています。

xmind使えるね!

image

オントロジーエディタ「法造」

image

ER図を作るのにオントロジーエディタを使ってみた

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

獲得タイトルとは

年度ごとに、打率、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問い合わせ

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

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