野球リーグスコア管理システムとキャップ野球情報局に関する進捗です。
旧システムについてはこちらの記事をご覧ください。
現行システム(ver.2)ではコメントアウトして提供していない機能ですが、作りかけの残骸を改修しています。
とりあえずログインはできるようになった。
ドメイン未定←これ重要。
フロントに予想以上に時間使ってバックエンド実装できず。
背景色透過は有料機能みたいです。
/league/リーグ名/season/シーズンIDというルーティングへの対応を進めています。新システムではリーグのURLをユーザが決められるようにする予定です。
NodeJSのrouterをこう書いてしまって404エラーしか返ってこなくなって嵌まりました。。。
this.router.get("/:league_name(\\d{1,20})/season/:season_id(\\d{1,3})", this.showStats);
旧システムのDBのダンプをスキーマを変えた新システムにリストアする方法を色々検討した結果、
一旦dockerにDBを2つ用意して新システムにinsert/selectすることにしました。
docker-composeで新たに設定を追加しました。
environment:
#タイムゾーンの設定
- TZ=Asia/Tokyo
#mysqlサーバの起動時に実行するコマンド
#文字セットの設定、SQLモードの設定
command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci --sql_mode="STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"
新スキーマに移行したのに伴い、DBアクセス周りのプログラムの改修が必要になっています。
interface作ったり、追加したテーブル用にservice(DBアクセスクラス)作ったり。
まだselect関係の機能を元に戻すので手一杯ですが、
insert/update関連の機能を早く実装したい。
去る6/8に行われた公式戦の動画作りましたのでよろしければご覧ください。
- 第一試合
5ケ月前の実装を引き継いでいます。git様様。
行を1つのコンポーネントにまとめて、ボタンで選手人数の変更に対応できるように実装しました。
ボタンは右に寄せようと思ってます。
あと数字入力コンポーネントは3桁入力できる幅でいいかなぁ。
サービス化するにあたって、サービス名の再検討とドメインの取得が必要になりますが、.jpドメインを取得するのでなければ、メール転送サービスがあるGoogle Domainsがよいとのアドバイスをいただきました。
上記に関連して、サービスリリースするならサービス名を再検討することにしました。
最近設計続きであまり外側からわかる進捗がないのですが、
プロジェクト全体の現在の状況です。
これまで、reactプロジェクトをnetlifyにデプロイするため、利便性の観点からreact/NodeJS(バックエンド)プロジェクトを分けていましたが統合しました。
create文(DDL)書き終わったので、現在は現行システム(ver2)のDBをver3システムに移行するSQLを書いているところです。
モダンな開発・運用環境ということでdocker-composeを使ってまずDBコンテナを立てています。順を追ってAPI用のNodeコンテナ、WEBサーバ用にnginxコンテナを立てる予定です。
XmindでリレーションとかPKの表現っぽいものができますね。
やはりマインドマップツールだけあって構想を練る時には便利。
現行システムは14テーブルですが、ほとんど使っていないテーブルがいくつかあるのと、内容的に重複するテーブルは統合しようかと思っています。
画面が結構多いので画面遷移図というよりはURLをどうするかの構想を起こしています。いくつか機能面もまとめています。
ちょっと慣れるまでに時間がかかりましたが、慣れてくると自由度が高いソフトですね。
意外と早めに仕様が固まったので早めに着手していけそうです。
現在、閲覧のみに特化した野球リーグ管理システムを公開していますが、機能的に追加開発するところが段々なくなってきました。
以上2点を大きな軸としてサービスリリースに向けて徐々に動き始めました。複数リーグを扱う上でDBの再設計は不可避です。
ということで論理設計からやり直しています。
年度ごとに、打率、HR、打点、安打数、防御率、勝利数、セーブ、奪三振数の各タイトルの保持状況を表したもの。
テーマ:楽しめる人を増やす
元々の実装では1位(タイトル獲得者)しか表示されず、
では一部の人しか面白くないと思ったので実装を変えることにしました。1-3位はメダル、10位以内入賞は順位を表示するよう変更しました。
集計処理はリプレース前の旧システムで保持していたのですが、
今回の仕様変更に伴いNodeJSで新たに実装しました。
変更前は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)
);
2/10ごろ完成したもの。
私の成績はそんなに上位ではないのですが、
最高で銅メダル(3位)取れていたのでちょっとうれしくなりました。
あまり難しいことはしてないのでまるっと公開してしまいますが。
おすすめサービスの候補が仮に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}
<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:'本システムの開発の進捗を掲載させていただいています。'
},
{....}
]
ただしトップページだけー!
メニューをカード表示にしたら意外ときれいだったのですが、
右上にスライドメニューも配置しているのでどうしようかと思っていたのですが、モバイルファーストの1カラムから2カラム構成へ変更しました。
ランダムで試合動画を表示する仕様でしたが、動画を見ていると「表示している動画と試合結果を連動したい」と思って、
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)
}
若干回りくどい実装のような気もします...。
元々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
ちょこちょこスタイルを直しました。
ちょっと後回しにしていた部分なのですが、スコアがデフォルトの青字リンクになっていたのを、
- 勝者を強調する
- フォントサイズ調整
- アイコンも15->20px
という形で修正しました。
メニューを2カラムにしてフォントサイズを調整しました。
フッタにもメニューがあるのでどうしようかと悩み中...。
自分一人で作っていると妥協してしまう点があるので、
誰かに見てもらって意見をもらうということはいいことですね!
内容的には昨日の進捗の続きです。
試合結果の右側のスペースが空いていたのもあり、
今シーズンのチーム上位3チームの成績を表示するようにしました。
テーブルコンポーネントは恒例のreact-bootstrap-table-nextです。
打者部門・投手部門で見出しのスタイルを変えるようにしました。
茶色の見出しは内野の土の色、白い見出しはベースの色をイメージしています。
また、あくまでスタッツのサマリなので、詳細に誘導するようリンクを作成しました。
データの都合上、シーズンが変わると「最新のスタッツ」のシーズンIDが変わるので、設定に
export const CURRENT_SEASON_ID = 41
と定数を設定してやり、「最新のスタッツ」のAPIを呼び出す際に定数を参照できるようにしました。
componentDidMount(){
fetchSeasonResult(CURRENT_SEASON_ID)
}
ハンバーガーメニューは一応用意しているのですが、
「気づきにくいかも」という指摘もいただいており、フッタにサイトマップを実装しました。
https://jcblscore-react.netlify.com/
ぜひ感想やご意見・フィードバックをお寄せください。
最新のスタッツへの直接アクセスが多い当サービスですが、トップページのコンテンツを充実させて刷新しました。
トップページにアクセスした際に「最新のスタッツ」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だと味気ないと思ったのですが。
https://jcblscore-react.netlify.com/
ぜひ感想やご意見・フィードバックをお寄せください。
これまで合計スコアのみの表示だった試合結果を、内訳まで表示するページを実装しました。
野球のスコアボードは後攻だけ色々なパターンがあって面倒なのです。例えば、試合を3イニングで打ち切った場合、
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>)
}
これまで蓄積したデータから毎年の月間打率を計算し、視覚化してみました。グラフのカラーセットを自動的に生成できると楽だなーと思うのですが。
この選手のグラフからは4,5月に好調を維持し、11月~2月あたりが不調という推測が成り立ちます。
qrcode.reactというコンポーネントを使って実装しました。
野球リーグの公式戦を開催する際に人数が不足していると、現地で試合に勧誘することがあります。よく口頭で「”カラーボール野球”で検索してください」と伝えてはいるのですが、やはりその場面で画面を見せてQRコードを読み取ってもらえると楽なのでは、と閃いて作りました。