体育会系Webエンジニアであることを生かしてスポーツライフをもっと楽しくしてみた

こちらは「[体育会系エンジニア Advent Calendar 2017](https://qiita.com/advent-calendar/2017/athlete)」の17日目の記事です。16日目は @mikity1985 さんの「[【Android】Firebase Remote Config で、アプリの挨拶文を変更する](https://qiita.com/mikity1985/items/2234cf4d86a939f79de5)」でした。

会社のお昼休みでテニスの壁打ちをしに行く程度にスポーツ好きのWebエンジニアの pawa と申します。

スポーツ歴は

* 野球
    * 中学3年間（軟式野球部）
    * 高校1年間（軟式野球部）
    * 草野球8年くらい（大学院での軟式野球サークル2年半を含む）
* 卓球：2017年2月に始めて9ヶ月目の区民大会で大敗を喫してから一時お休み中
* テニス：2017年9月11日に始めて3ヶ月目くらい

という具合で、今はテニスを週2＋自主トレ、野球を月1くらいでやっています。卓球・テニスは、40・50・60・70歳と年を取ってもスポーツをしていたくて始めました。20代後半から未経験のスポーツを始めるのは勇気が必要でしたが、これが中々刺激的で、新たな出会いもあり、挑戦して良かったと思っております。

なぜスポーツをするのか……

* 試合に勝ったあとのお酒の異常なほどのおいしさ
* ラリーが続いてお互い笑顔にならざるを得ない楽しい感覚
* ゾーンに入って周りの景色がスローになって自身でも驚くようなビッグプレーをしていたときの快感
* ビッグプレーをした後の対戦相手・観客の驚く顔を見てゾクゾクする感じ

などは一度覚えるともう止められません。やみつきです。これらの一瞬を目一杯に味わうためにスポーツをしています。

# 体育会系Webエンジニアであることを生かしてみた

社会人になってもスポーツをしているWebエンジニアである強みが一番生きる場面は、やはりチームのWebサイトを作る場面ではないでしょうか？　私が属する草野球チームでもチームのWebサイトを作らないかという話が出ました。誰が作るかとなると、当然のごとく、チーム内に極稀にいるエンジニアが作ることになります。（__Webエンジニアリングもスポーツもバリバリやる人はかなり希少価値が高い__ようです。）

## 草野球チームのWebサイト作り

草野球は競技人口も多く、草野球チームのWebサイトを画面上でポチポチするだけで作れるサービスもあります。それらのサービスを使う提案も行ったのですが、チームオリジナルのWebサイトを作りたいという声が圧倒的多数派だったため、スクラッチで開発することにしました。開発時間帯は主に25時〜28時の間の2時間です。（気合だ!）

### 社会人スポーツチームのWebサイトに必要なものは何か

「どんな内容を掲載すればチームのWebサイトとして機能するか」を最初に考えました。

訪問者は大きく分けて以下の3種類になると考えました。

* チームメンバー
* 所属したいチームを探している方
* 対戦相手を探しているチームの方

それぞれが何をできればいいかを考えると以下のように考えられました。

チームメンバーが…

* チームの活動を確認できる
* 成績を確認できる

所属したいチームを探している方が…

* チームがアクティブであることを確認できる
* どんなメンバーが所属しているか確認できる
* 入団希望である旨をチームメンバーに連絡できる

対戦相手を探しているチームの方が...

* チームがアクティブであることを確認できる
* どんなメンバーが所属しているか確認できる
* 試合申込できる

以上より、大まかに

* チーム紹介
* 選手名鑑
* ブログ
* 試合申込
* 入団希望
* 日程表（←最近管理できていない）

というメニューがホームにあれば、上記3種類のどの訪問者にも簡潔に対応にできると考えました。

### 技術構成

以下の構成で作ってみました。

* さくらのVPS 2G
* Nginx
* Perl（Mojolicious）
* jQuery（「日程表」表示部分）
* HTML5, Compass・SCSS
* Munin, Monit（サーバ監視）
* WordPress（ブログ）
* SQLite3（成績管理）

### デザイン

チームのユニフォームのカラーを意識しつつ「迷わせない」ことを意識して「[Sketch](https://www.sketchapp.com/)」でデザインしました。

<img width="375" alt="写真 2017-12-05 2 17 08.png" src="https://qiita-image-store.s3.amazonaws.com/0/28110/44301556-a89d-1667-babe-c8b8618abc9b.png">　<img width="375" alt="写真 2017-12-05 2 16 44.png" src="https://qiita-image-store.s3.amazonaws.com/0/28110/1fa28871-e12a-3185-6511-97a7d402412a.png">

ロゴはチーム内のデザイナーの方が作ったようで、ロゴ画像だけもらいました。（当時はLINEで画像を送ると激しく劣化したので、管理画面にアップローダを設置して、そこからロゴ画像を送ってもらいました。）

### Google Calendar API を利用した日程表

会社の仕事で Google Calendar を使っているので、仕事の合間に更新できるように、Google Calendar に予定を追加するとチームの日程表ページも更新される仕組みが欲しいと思いました。実現方法を調べると「[Google::API::Client](https://metacpan.org/pod/Google::API::Client)」「[Monthly.js](http://kthornbloom.com/monthly/)」を使うといい具合にレスポンシブデザインで実現可能であることが分かりました。

チョチョイのチョイとPerlのコードを書いてやると、休日・祝日用のカレンダーデータとチームの練習・試合予定がいい具合に混ぜ合わさった Monthly.js 用のXMLを出力できました。（今だとJSONでも対応していると思います。）

`client_secret.json` を取得するのが少々面倒だった覚えがあります。（ググって Google API のページから取得してください😅）

```perl:Monthly.js用の日程表をXMLで出力するPerlスクリプト
#!/usr/bin/env perl

use strict;
use warnings;
use utf8;
use feature qw/say/;
use open qw/:encoding(utf-8) :std/;
use JSON qw//;
use Time::Moment qw//;
use Google::API::Client;
use Google::API::OAuth2::Client;
use XML::Simple ();
use Cache::FileCache;
use DDP; # cpanm Data::Printer してね

my $myemail = 'xxx@gmail.com';
my $client_secrets_file = 'client_secret.json';
my $client = Google::API::Client->new;
my $gcal = $client->build('calendar', 'v3');
my $auth_driver = Google::API::OAuth2::Client->new_from_client_secrets($client_secrets_file, $gcal->{auth_doc});

my $dat_file = 'token.dat';
get_or_restore_token($dat_file, $auth_driver);

my $my_calender_id      = 'primary';
my $holiday_calendar_id = 'ja.japanese#holiday@group.v.calendar.google.com';

my $cache = Cache::FileCache->new({
    cache_root => '/tmp',
    namespace  => 'calendar',
});

my %cache_duration_of = (
    $my_calender_id      => '5s',
    $holiday_calendar_id => '7d',
);

my $now = Time::Moment->now;

my $first_day_of_month = Time::Moment->new(
    year   => $now->year,
    month  => $now->month,
    day    => 1,
    offset => 540, # +09:00（540分）
);

my $req_body = {
    timeMax => $first_day_of_month->plus_months(6)->to_string,
    timeMin => $first_day_of_month->minus_months(6)->to_string,
};

my $schedule = get_or_cache_event_items($my_calender_id);
my $holidays = get_or_cache_event_items($holiday_calendar_id);

my @events = (@{$schedule->{items}}, @{$holidays->{items}});

print make_xml(\@events);

exit;

sub get_or_restore_token
{
    my ($file, $auth_driver) = @_;

    if (-f $file)
    {
        open (my $fh, '<', $file) or die $!;
        local $/;
        my $access_token = JSON::from_json(<$fh>);
        close($fh);

        $auth_driver->token_obj($access_token);
    }
    else
    {
        my $auth_url = $auth_driver->authorize_uri;
        say 'Go to the following link in your browser:';
        say $auth_url;

        say 'Enter verification code:';
        chomp(my $code = <>);
        $auth_driver->exchange($code);
        store_token($file, $auth_driver);
    }
}

sub store_token
{
    my ($file, $auth_driver) = @_;

    my $access_token = $auth_driver->token_obj;

    open (my $fh, '>', $file) or die $!;
    print {$fh} JSON::to_json($access_token);
    close($fh);
}

sub get_or_cache_event_items
{
    my $calendar_id = shift;
    my $items = $cache->get($calendar_id);

    if ( ! $items )
    {
        $items = $gcal->events->list(calendarId => $calendar_id, body => $req_body)->execute({ auth_driver => $auth_driver });
        $cache->set($calendar_id => $items, $cache_duration_of{$calendar_id}) if $items;
    }

    return $items;
}

sub get_date_using_datetime { local $_ = $_[0]; s/(.+)T.+/$1/; $_; }
sub get_time_using_datetime { local $_ = $_[0]; s/.+T([0-9][0-9]:[0-9][0-9]):[0-9][0-9]\+.+/$1/; $_; }

sub make_xml
{
    my $items = shift;

    my $data = {};
    $data->{event} = ();

    my $i = 0;

    for my $item (@{$items})
    {
        my $event = {};

        my $startdate = $item->{start}{date} ? $item->{start}{date} : get_date_using_datetime($item->{start}{dateTime});
        my $enddate   = $item->{end}{date}   ? $item->{end}{date}   : get_date_using_datetime($item->{end}{dateTime});

        $event->{id}        = ++$i;
        $event->{name}      = $item->{summary};
        $event->{startdate} = $startdate;
        $event->{enddate}   = $enddate;

        $event->{color} = '#ffb128';
        $event->{color} = '#ddeeff' if $item->{organizer}{email} ne $myemail;

        $event->{starttime} = get_time_using_datetime($item->{start}{dateTime}) if $item->{start}{dateTime};
        $event->{endtime}   = get_time_using_datetime($item->{end}{dateTime})   if $item->{end}{dateTime};

        push(@{$data->{event}}, $event);
    }

    return XML::Simple::XMLout($data, RootName => 'monthly', XMLDecl => '<?xml version="1.0"?>', NoAttr => 1);
}
```

このプログラムをWebアプリケーションに組み込んで Monthly.js に処理させると、以下の画像のように日程表ページが出来上がりました。

<img width="375" alt="写真 2017-12-16 19 07 19.png" src="https://qiita-image-store.s3.amazonaws.com/0/28110/f28bb543-e5bf-50ff-425c-a967de902128.png">

### みんな大好き管理画面

管理画面は以下が行えるだけのごく単純な仕様になりました。

* HP素材のアップロード
* メンバー追加
* メンバー編集

<img width="375" alt="写真 2017-12-16 18 25 52.png" src="https://qiita-image-store.s3.amazonaws.com/0/28110/4f1baa87-6e6a-0d40-c080-5c44c3080e7d.png">

メンバーの成績の更新は管理画面の「メンバー編集」から行います。1試合ごとに記録しているわけではなく、累積値を更新していく手抜き仕様になっています。

<img width="375" alt="写真 2017-12-16 19 23 14.png" src="https://qiita-image-store.s3.amazonaws.com/0/28110/478be709-4757-8808-6168-733f53f92ff9.png">

試合後にマネージャからスコアシートをいただいて、間違いがないか、かなり神経を使って更新していっています。

直近の試合のスコアシート↓
![score.jpg](https://qiita-image-store.s3.amazonaws.com/0/28110/db2edbc4-fb3a-9aa6-b5e3-a50af35b3bef.jpeg)

### 作って良かったこと

* いろんなチームメンバーと積極的に関わることができる
* 打率・防御率・盗塁王争いなどの話題で試合中・試合前後の雑談が盛り上がる
* 試合申込ページからの申し込みで実際に試合が行われた
* 入団希望者が入団希望のページから連絡してくださって実際にチームに入ってくれた
* 試合を申し込むときに誇らしくWebサイトの独自ドメインURLを相手チームに伝えられる

いろいろ大変なこともありすぎたけど、作って良かったことのほうが間違いなく多かったです。（本当です！）

## 私の今後の体育会系Webエンジニアであることの生かし方

別の草野球チームのつながりで、スポーツ組織のWebサイト製作の依頼を有料で引き受けました。とりあえずはこの依頼を着実に成功に導く予定です。

# 終わりに

この記事は体育会系Webエンジニアであることの「私」の生かし方という一例に過ぎません。
読者様の得意な技術でスポーツライフをもっと楽しくしてみませんか？

12月18日は @kosukehomma さんのターンです！