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

2019-01-13に作成

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

使っている技術など

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

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

残りタスクリスト

trello

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

バえない機能のつくりかた

割と思い付きで機能を作ってしまう方なのですが、
(旧バージョンの情報局でムダとなった機能をリニューアル時に捨てた)
今回はブラウザで作れる野球選手カードについて。

CSSで画像を装飾する

フレームを作る


画像を見てやり方に気づいた方はご名答。
角のフレームは四角形を傾けて重ねてるだけなのです。
transform 15degで15度傾けて表示しています。
親要素をrelativeにし、この要素自体をabsoluteにすると
位置調整ができます。

<div
            style={{
              position: "absolute",
              transform: "rotate(15deg)",
              bottom: -40,
              right: 5,
              width: 350,
              height: 100,
              backgroundColor: "色を指定",
            }}
          />

はみ出した部分をカットする

親要素の大きさを指定して、overflow:hidden
とすると、はみ出した要素を非表示にできます。

<div
        style={{
          width: 340,
          height:450,
          backgroundColor:'#e6e6e6',
          overflow: "hidden",
          position: "relative",
        }}
      />

写真に丸アイコンを載せる

div要素のbackgroundImageに画像を指定して、
border-radius 50%で丸にします。
div要素の高さはwidth,heightで指定し、
画像の大きさはbackground-sizeで調整します。

親要素をrelativeにし、この要素自体をabsoluteにすると
位置調整ができます。

<div
            style={{
              width: 80,
              height: 80,
              border:'2px solid white',
              backgroundSize: "80px 80px",
              backgroundImage: `url('画像のURL')`,
              backgroundRepeat: "no-repeat",
              backgroundPosition: "center center",
              position: "absolute",
              display: "inline-block",
              borderRadius: "50%",
              top: 3,
              left: 3,
            }}
          />

CSSで遊ぶって楽しい!

これまでも色々CSSで遊んできたので比較的サクッと実装できました。(ただし不評)

キャップ野球情報局v2.0リリースしました。

キャップ野球情報局」というサイトを作っています。

アップデート履歴

ちまちまとマイナーアップデートを重ねていましたが、
5/17からNextJSへ移植するのと同時に、次のメジャーアップデート(v2.0)を7月リリースを目標に作っています。

次バージョン新機能

twitterアカウントを持っていれば誰でもログインできます。

マイページ

ユーザページ編集

別サービス「みんなのSCORE」のデータに対応する形で選手ページを持っています。

選手へのコメント機能

選手間で「この選手はどういう選手です」という他己紹介をする機能です。

チームページ編集

別サービス「みんなのSCORE」のデータに対応する形でチームページを持っています。
チームの紹介のほか、チームのテーマカラーが設定できます。

イベント登録・編集

キャップ野球には、主なイベントとして

  • 大会
  • リーグ
  • 練習会

がありますが、それらの情報を登録・編集することができます。

次バージョンに採用している技術

  • フロント
    • NextJS
    • ReactJS
    • Netlify
    • TypeScript
  • サーバサイド
    • NodeJS
    • MySQL
    • Docker(-compose)
    • TypeScript
  • ミドルウェアなど
    • slack
    • firebase
    • cloudinary

imgurではなくcloudinaryを採用した理由

運営者ギルドでは画像ストレージとしてimgurを薦められていたのですが、imgurとcloudinary両方を実装して、使いやすさの観点からcloudinaryを採用することにしました。

ログイン

imgurはOAuth認証すればログイン状態でアップロードできるのですが、ブラウザでPINが必要など使い勝手と実装に難がある印象です。

匿名アップロード

比較的実装が簡単ですが、imgurでは匿名アップロードした画像をGUIでは管理できません。

工数的にはcloudinaryのログイン状態アップロード≒imgurの匿名アップロードという印象だったので、GUIで全体管理ができるcloudinaryを選びました。用途としては無料枠で足りると思います。

2つのアプリケーションに共通の項目はどこに保持するのがよいか

考え方の変遷

以前、こういう記事を書いたのですが、

設定を変えるたびにnpm installしないと反映されないのが不便だったのもあり、同じDB、同じアプリケーションサーバを使っているならば、即座に反映されるDBに保持するのがよいという考えに変わりました。

2つのアプリケーションで共通していてテキストファイルで保持していたデータは以下の2つです。

  • チームの公式twitter
    • チームテーブルにカラム追加
  • 試合ごとのyoutube 動画URL
    • 試合テーブルにカラム追加

試合ごとのyoutube URLは自分でDB設計を変えたときに用意していたみたいですが、完全に存在を失念していました。

特段新しい技術の導入はしてないです。

みんなのSCOREがver4αになりました。

NodeJS側

続・Twitterログイン

以下のパッケージを使う。
- passport-twitter
- twitterログインを行うライブラリ
- express-session
- ログイン情報をセッション(メモリ)に格納するライブラリ。
- express-mysql-session
- 揮発するメモリのセッション情報をmysqlに格納するライブラリ。

reactJSからAPI(NodeJS/Express)へはcredentialsを要求するようにする。

データ保全の強化

ユーザが書き換えるテーブルに履歴テーブルを用意した。
これでこのプロジェクトの合計テーブル数は21となった。

ユーザの権限周りの実装

これまで権限レベルが単一だったが、今回権限を制限したユーザを想定しているので権限周りの実装を行った。

ReactJS側

自動入力コンポーネントの移行

スコア入力で大人数の中から選手を選ぶのに自動入力コンポーネントを使用している。

react-autocompleteはどうもモバイルフレンドリーではなかったようで、モバイルでサジェストが見えなくなってしまう欠点があったので使用するコンポーネントを移行した。

SCSS化

他プロジェクトではすでに導入しているSCSSをこのプロジェクトでも導入。最も古参プロジェクトだったのでCRA(react-scripts)のバージョンを上げるなど。

その他

みんなのSCORE、version4開発開始

長らくボードを放置していましたが、
みんなのSCOREのversion4に向けて開発を開始しました。

4/15修正内容

三塁打対応

  • OPS
  • 長打率

の計算を三塁打を加味したものに修正。

セイバーメトリクス

メンテナンスの都合上、セイバーメトリクスの計算をSQLからTypescript側で行うように修正。

修正前(SQL)

        CASE
            WHEN ((sum(hit)+sum(twobase)*1+sum(homerun)*2)/sum(at_bats)) is null THEN 0
            WHEN ((sum(hit)+sum(twobase)*1+sum(homerun)*2)/sum(at_bats)) is not null THEN ((sum(hit)+sum(twobase)*1+sum(homerun)*2)/sum(at_bats))
        END as slg,
        CASE
            WHEN ((sum(hit)+sum(four_ball))/sum(tpa))+((sum(hit)+sum(twobase)*1+sum(homerun)*2)/sum(at_bats)) is null THEN 0
            WHEN ((sum(hit)+sum(four_ball))/sum(tpa))+((sum(hit)+sum(twobase)*1+sum(homerun)*2)/sum(at_bats)) is not null THEN ((sum(hit)+sum(four_ball))/sum(tpa))+((sum(hit)+sum(twobase)*1+sum(homerun)*2)/sum(at_bats))
        END as ops,
            (sum(hit)+sum(four_ball))/sum(tpa) as obp,
            ((((sum(hit)+sum(twobase)*1+sum(homerun)*2)+0.26*sum(four_ball)-0.03*sum(strike_out)+3*sum(tpa))*(sum(hit)+sum(four_ball)+2.4*sum(tpa)))/(9*sum(tpa))-0.9*sum(tpa))*27/(sum(at_bats)-sum(hit)) as RC27,
            CASE
            WHEN sum(at_bats)/sum(strike_out) is null THEN 0
            WHEN sum(at_bats)/sum(strike_out) is not null THEN sum(at_bats)/sum(strike_out)
        END as not_strike_out,
        CASE
            WHEN sum(at_bats)/sum(homerun) is null THEN 0
            WHEN sum(at_bats)/sum(homerun) is not null THEN sum(at_bats)/sum(homerun)
        END as avg_homerun,
        CASE
            WHEN sum(rbi)/sum(at_bats) is null THEN 0
            WHEN sum(rbi)/sum(at_bats) is not null THEN sum(rbi)/sum(at_bats)
        END as avgRbi,

修正後

public calcSABR = (players) => {
    for(let i = 0; i < players.length; i++){
      players[i].average = players[i].hit/players[i].at_bats;
      players[i].slg = (players[i].hit + players[i].twobase*1 + players[i].three_base*2 + players[i].homerun*3 )/players[i].at_bats;
      players[i].obp = (players[i].hit + players[i].four_ball)/players[i].tpa;
      players[i].ops = players[i].slg + players[i].obp;
      const rc27b = players[i].slg * players[i].at_bats + 0.26 * players[i].four_ball - 0.03 * players[i].strike_out;
      const rc27a = players[i].slg * players[i].at_bats + players[i].four_ball;
      const rc27c = players[i].at_bats + players[i].four_ball;
      players[i].rc27 = ((rc27a + 2.4*rc27c)*(rc27b + 3*rc27c)/(9*rc27c))-(0.9 + rc27c);
      players[i].not_strike_out = players[i].at_bats/players[i].strike_out;
      players[i].avg_homerun = players[i].at_bats/players[i].homerun;
      players[i].avgRbi = players[i].rbi/players[i].at_bats;
    }
    return players;
  }

残りのversion4 実装予定機能

スコア入力一般開放

  • twitterログイン
    • 先日の蓋々交換で技術調査・実装済み
  • スコア入力画面レスポンシブ対応

要件

チーム管理者は

  • 自分のチームの成績のみ入力できる
  • チームメンバーを追加できる
  • メンバーのサジェストを自チームに限定する

その他バグ修正

リファクタリング

テストコード実装

意見募集

みんなのSCORE、キャップ野球情報局はユーザの皆さんのご意見を募集しています。

1/9進捗

サービスURL

イベント登録フォーム設置

DBに下書き状態でイベント情報を登録するフォームです。
bot対策にrecaptchaを採用しました。

reactJSでgoogle recaptchaを使う

site keyを取得する

コンポーネントを設置する

<ReCAPTCHA
    sitekey="Your client site key"
    onChange={onChange}
  />

レスポンシブ対応

PC画面での表示を2カラムにしました。

1/2 キャップ野球情報局リニューアル

2020.jpg
新年2本目のボード投稿になります。
(新年の実績としては1本目)

キャップ野球情報局

https://cap-baseball.com/

成績管理システムとの連携

今回の改修の目玉は、成績管理システム「みんなのSCORE」との連携です。
成績管理システムのデータベースを利用しているのでいわば兄弟サイトです。

試合結果の表示

  • リーグ戦
  • 練習試合
  • 大会(大福大会、佐倉大会など)

の結果をトップページで閲覧できるようになりました。
右下のページング部分から10件ずつ遡ることが可能です。

試合結果詳細

試合結果とともに関連するツイートの表示と
(あれば)試合動画の表示を行います。

チームページ


例:一橋大のページ
通算成績と直近の試合結果が表示されます。

使っている技術

  • ReactJS
  • Netlify
  • NodeJS
  • MySQL
  • Docker
  • nginx

今後の課題

  • 一回見ると飽きてしまう(リピーターがいない)
  • ブログ記事などの集約・紹介
  • コンテンツなどの充実
  • 個別記事とチームの紐づけ

さよなら自宅サーバー

経緯

これまで、フロントはnetlify、バックエンドは自宅サーバーという構成でサービス提供していたのですが、某アップデートの際にOSの再起動でdockerが止まって度々サービス断が発生してしまうのが段々いやになってきたのと、インフラの基礎ができてきたのでconoha VPSでインスタンス作ってサーバ移行しました。

サーバ構築

conoha VPSでubuntuインスタンスを作る

初期設定をする

DNSの設定をする

SSL証明書を発行する

またしてもLet's encryptに苦しめられました。今回はdocker-composeの中のnginxコンテナに適用するつもりでした。調べたところ、証明書生成用のコンテナは別途あったのですが、わざわざdocker-composeの構成書き直すのも...と思ったので、
nginxだけdockerコンテナを使わない構成に変更しました。

CMS風サイトを作りました

ogp.jpg

CMS(ブログ)風サイトを作りました

以前、7月ごろに技術ブログを作ってみたという記事を書いたのですが、結局保守が面倒ということでボツにしまして。

URL

https://cap-baseball.com/

機能

トップページ

キーワードによる記事の絞込みが可能です。
新着記事とキーワードで探すメニューを用意しています。

記事ページ


主に試合結果を掲載しています。youtubeの埋め込みはmarkdownにソースを挿入するだけで問題なく動作しました。
あと、1文字目の色を変えるところに凝りました。

h1::first-letter{
  color: #468847;
}

技術

今回は記事を簡単に書けること(markdownとか)を目指して、ヘッドレスCMSを調査したのですが、コンポーネントの再利用を考えるとwordpressは除外、strapiやcontentfulも試してみたものの、データの準備に時間がかかり過ぎるので今回は見送りました。今回もDBレスで、マスタデータはjsonです。

ヘッドレスCMS

Kosugi Maruフォントへの移行

優勝ラインシミュレーター作りました。

急遽クソアプリ2 Advent Calendar 2019の1日目に参加することになりました。

計算を自動化するスプレッドシートを作っていて、やっぱりReactで書いた方が動的にスタイル変更できるしいいじゃない!と思って作りました。

どんなアプリ?


試合数、チームの成績をもとにリーグ戦の優勝ラインをシミュレーションします。勝率と勝ち点方式に対応しています。

制作期間

2019/12/01~
突貫で最低限の機能だけ作ったので今回はUIに凝っていないです。

URL

https://championship.netlify.com

工夫したところ

結果のテーブルヘッダを固定してスクロールできるようにしています。元々<table>タグで書いていたのですが、固定しようとすると横幅が一致しない問題が出てきたので<div>タグで初めてテーブル書きました。

div.table_header{
  display: table-row;
}
div.table_cell{
  display: table-cell;
  border: 0.5px solid #696969;
  padding: 2px;
  min-width: 4rem;
  text-align: right;
  white-space: nowrap;
}
div.scroll{
  overflow-y: scroll;
  height: 80vh;
}

把握している課題

  • UI
  • 試合数全てのパターンを計算・表示しているため、試合数に大きな数字を入れるとブラウザがメモリ不足で落ちる。
    • いずれかのチームに該当する結果があるまで表示しないようにする
  • チームの列入れ替え機能欲しい

一日で作るReactNativeアプリ

Screenshot_20191104-185009_pixel_quite_black_portrait.png

審査通りました!(11/7)

3日かかりましたが審査通りました!

開発の経緯

リーグ参加者にも少数ではあるがソフトウェア開発に知見がある人がいて、こういうのが欲しいというプロトタイプ(上記)まで見せてもらった。

作ったもの

開発に用いたライブラリ

deploygateならiOSもコストなく配布できると思っていた

Xcodeでarchiveはできる
dg deploy(deploygateにipaを上げるコマンド)
→iOSアプリ配布用の証明書が必要といわれる
→iOS developer programに入ってないと作れないよ
→iOS版断念...orz

Google Play Developer Consoleに登録した

当初は野良APKで配布するつもりだったのですが、iOS developer programほどひどいライセンス体系ではない(登録時に$25)ので登録しました。「提供元不明」だと誰も使ってくれなさそうだったので。

審査に出しました

いろいろ記入するところあって予想以上に大変だった....

10/23フィードバック対応

同率順位非表示の修正

ikenにフィードバックいただきました。ありがとうございます。

スタッツ画面で、チーム順位タブでは同率の場合に順位表示が消えますが、例えば3位が2チームある場合は「同率3」などの表現だと、試合数や勝率で並び替えた場合にも順位がわかり、わかりやすくなるかもと思いました。安打数とかもですかね?(並び替えはできないですが)

そもそもソートを想定していなかったのですが、投手部門だけ項目を増やした結果、ソート時に順位が非表示になっていると順位でソートし直せなかったり、どの順位タイなのかがわからなくなっていたのでありがたいご指摘でした。

引き続きご意見募集してます。

10/20進捗

ikenにフィードバックいただきました。ありがとうございます。

トップページの、現在提供中のデータ欄からデータに飛べますが、飛べることがちょっとわかりづらいかもです。例えば、[データを見る]と書かれたボタンの画像が、今あるそれぞれの画像の上に合成されると、押しやすくなるかも?と思いました。あと、最新3試合の結果欄で、何対何以外の部分をクリックした場合も、詳細画面に移ると楽かもと思いました。

チーム順位はみんな特に気にするかなと思うので、TOP3よりもうちょっと表示されてると嬉しいかもと思いました。

・「最近の試合結果」カード自体をクリックできるように修正
・チーム順位などの項目を5件まで表示

・「提供中のデータ」の選択状態がわかるように修正、画像右下に文言追加

・「チームページ」の直近の試合結果のUI崩れ修正

運営してくれているというのは競技者にとって、とても嬉しいところだと思います。応援してます!

ありがとうございます!

10/12-14進捗

image

ランディングページの実装

カルーセルのtouch対応

先日の記事でreact-slideshowを推しましたが、スワイプに対応していないという問題があったのでいくつかtouchイベントに対応しているカルーセルコンポーネントを調査しました。

react-slick、君に決めた!

LINE風CSSの適用

会社の同僚にアイデアをもらったので自力で実装してみました。

 <div style={{
        padding:5
       }}
     >
            <div className="question">掲載料金はかかりますか?</div>             
            <div className="answer">いいえ。料金はかかりません.</div>
            <div className="question">データはどうやって登録するのですか?</div>
            <div className="answer">現在、運営がデータを入力する形となっていますのでデータをお送りください。</div>
        </div>
.question{
  width: 70%;
  position: relative;
  padding: 10px;
  background-color: #f2f3f7;
  font-size: 16px;
  color: #231815;
  border-radius: 12px;
  box-sizing: border-box;
  margin: 5px;
}
.answer{
  width: 70%;
  position: relative;
  left:50px;
  padding: 10px;
  background-color: #fde5e5;
  padding: 10px;
  font-size: 16px;
  color: #231815;
  border-radius: 12px;
  box-sizing: border-box;
  margin: 5px;
}

webフォントの軽量化

https://gist.github.com/manabuyasuda/b5c867a7cbd17d1eb905b3a8cfd621a6

flexboxを実装する


今回はグリッドレイアウトを使わずにflex-boxを使いました。
幅500pxを境界に、flex-directionを切り替えるメディアクエリを書きます。flex-directionを切り替えるのと同時に画像をウインドウの50%か100%に切り替えます。
サイズの異なる画像を並べるのにobject-fit : coverが便利でした。

JSX

const imageLink = (toLink,image_url,text) => {
    return(
          <div 
            style={{
              position:'relative'
            }}
          >
            <div
              style={{
                fontSize: 20,
                cursor:'pointer',
                position:'relative'
              }}
              onClick={()=>{
                window.location.href= toLink
              }}
            >
              <img 
                style={{
                  height:'20vh',
                  objectFit:'cover'
                }}
                src={image_url}/>
              <span
                  style={{
                    fontSize: 20,
                    color:'aliceblue',
                    textShadow:'2px 2px 2px black',
                    position:'absolute',
                    top:0,
                    left:0
                  }}
                >{text}
              </span>
            </div>
          </div>
    )
  }

CSS

@media(max-width:500px){
  .flex-parent{
    display: flex;
    flex-direction: column;
  }
  .flex-parent img{
    width: 100vw;
  }
}
@media(min-width:501px){
  .flex-parent{
    display: flex;
    flex-direction: row;
  }
  .flex-parent img{
    width: 50vw;
  }
}

参考
- Flexbox【第1回】並べる方向 〜flex-direction編〜
- 縦横比の違う画像を均等に横並びにする方法
- 画像の上におしゃれに文字やボタンをのせる方法

10/6進捗

概要

  • 動画スライドコンポーネントの実装
  • 蓋ざんまいサイトとの連携
  • 独自ドメイン取りました
     - トラブルシューティング
  • 日別試合結果累計バグ対処
    • 原因

動画スライドコンポーネント

主にキャップ野球用の対応ですが、皆さんyoutubeとかtwitterにばらばらに動画を上げているので、蓋ざんまい非公式サイトではその集約の役割も果たしていました。

ただ、蓋ざんまいサイトでは
- 縦列に配置してコンテンツが長くなってしまう

という欠点があり、試合結果を一律JCBL-SCORE側で表示する対応をするため、どんなUIがいいか考えた結果、カルーセルみたいに横にスライドするのがよいという考えに。

使ったコンポーネント

  • react-slideshow
    • 今回新たに採用したコンポーネント
  • react-youtube
    • JCBL-SCOREで既に採用
  • react-twitter-embed
    • 蓋ざんまいサイトで既に採用

実装結果

蓋ざんまいサイトとの連携

蓋ざんまいで独自に実装していた試合結果ページを廃止(アクセスはできますがリンクは張っていません)して、一律JCBL-SCORE側で試合結果を表示するようにしました。

独自ドメイン取りました

  • 旧ドメイン(netlify)
    • https://jcblscore-react.netlify.com/league/cap_baseball
  • 新ドメイン
    • https://jcbl-score.com/league/cap_baseball

だいぶ短縮できました。
設定自体は大して難しくなく、ちょっと古いですが以下のサイトなどを参考に進めました。

手順としては、
1. ネームサーバの設定をnetlifyのものにする
2. DNSが浸透するまで待つ
3. Let's encryptでSSL証明書を発行する(netlifyがやってくれる)

トラブルシューティング

3でちょっとはまりました。
ERR_CERT_COMMON_NAME_INVALID(プライバシーエラー)が出てしまうのです。
作られた証明書を見ると、subjectが「*.netlify.com」になっています。証明する対象が間違っているわけです。
よくよく考えると、新しいドメインにリダイレクトしていないままLet's encryptの処理をしていたので、redirects

[https://jcblscore-react.netlify.com/*](https://jcblscore-react.netlify.com/*) https://jcbl-score.com/:splat 301!

1行追記します。これで証明書を発行し直すとプライバシーエラーが解消されました。

日別試合結果累計バグ

原因

APIコンテナとDBコンテナのタイムゾーンがGMTで、DBに入っている値がJSTだったので、日付で比較しようとすると、SQLで>や
composeファイルで書くと有効な対応策がないので、SQLに渡す際に日付オブジェクトにするのをやめました。