Google Apps Scriptで競技かるたの読み札をWebアプリ化した

# 作ったもの

[**https://script.google.com/macros/s/AKfycbwXI-uiPir7LjSJcGAE-58Zcry5MLHMvPYUmE45XvBoyiIRZ_msKi-hhh5D_u1TTeg/exec**](https://script.google.com/macros/s/AKfycbwXI-uiPir7LjSJcGAE-58Zcry5MLHMvPYUmE45XvBoyiIRZ_msKi-hhh5D_u1TTeg/exec)

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632954/c91de1b1-4dbe-cd9b-78dd-cac9e2aaed7e.png" width="200px">

### 機能

- 競技かるたの読み札をランダムな順番で表示します
    - 読唱規定に則った読み方を表記しています
- 混ぜ直したいときは上のシャッフルボタンを押します
- どこまで読んだか確認できるように，読み札をタップで色をグレーに変えられます

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632954/d2f0245d-14f3-c7c1-60c7-6d4042583f81.jpeg" width="300px">

### 作った経緯
趣味で競技かるたをしており、読み手をやることもあります。
読み札が手元にない場合もあるので，スマホで手軽に使えたら便利だと思い，作りました。

# 準備
サーバーを用意する必要がなく，ブラウザ上で簡単に作れそうなので，GoogleスプレッドシートとGoogle Apps Script(GAS)を組み合わせて作りました。

## スプレッドシート

https://docs.google.com/spreadsheets/d/1KXzH14CZRP0oiv1_WImCber2X94bjYzKp6AewolcKvY/edit?usp=sharing

### fuda_list シート

- 百首の歌番号と和歌を入力するマスタシートです
- 改行するための br タグ，色をつけるための font タグ，ふりがなを振るための ruby タグはセル内に入力してしまっています

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632954/e700439a-80a4-e91b-a479-9e66448ed287.png)

### yomi_list シート

- 乱数から読み上げの順番を生成しています
- 合わせて，ここでチェックボックスや，何枚目かの表記を各セルの1行目に入れています

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632954/61369c20-bbdd-d923-f426-d000cdc6d28c.png)

### input シート

- シャッフルで乱数の再計算をさせたいときに，GASからこのシートに適当な値を入力するために用意しています

### output シート

- 1行目には，競技かるたで最初に読み上げる序歌を入れています
- 2〜101行目に，読み札に含める内容を yomi_list シートから引っ張ってきています

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632954/aa196ef1-435d-848b-deb4-53aaf3d96beb.png)


## GAS

GASは，スプレッドシートのコンテナバインドスクリプトとして作っています。
このように，GAS・HTML・CSS・JavaScriptでファイルを分けています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632954/1e196862-57c0-4e91-aa1b-19ba092df2f4.png)

### GASのコード

```javascript:コード.gs
const ss = SpreadsheetApp.getActiveSpreadsheet();

// index.htmlをテンプレートとしてHTMLを出力する関数。PWA化に必要なメタタグもここで入れます。
function doGet() {
  let output = HtmlService.createTemplateFromFile("index").evaluate();
  output.addMetaTag('viewport', 'width=device-width, initial-scale=1')
        .addMetaTag('apple-mobile-web-app-capable','yes')
        .addMetaTag('mobile-web-app-capable','yes')
        .setTitle("月読(読み札)")
        .setFaviconUrl('https://drive.google.com/uc?id={Googleドライブ内のファビコン画像のIDを入れます}&.png');
  return output;
}

// inputシートに「1」を入力することで再計算させ，読み札をシャッフルし直す関数
function reloadSheet() {
  ss.getSheetByName("input").getRange(1,1).setValue(1);
}

// outputシートから読み札の一覧を取得して返す関数
function generateYomiList() {
  return ss.getSheetByName("output").getRange(1,1,101).getValues();
}

// スクリプトのURLを取得する関数（リロードに使用）
function getScriptUrl() {
  let url = ScriptApp.getService().getUrl();
  return url;
}
```

### HTML

`<?!= HtmlService.createHtmlOutputFromFile("js").getContent(); ?>`
`<?!= HtmlService.createHtmlOutputFromFile('css').getContent(); ?>`
上記はそれぞれ`js.html`や`css.html`の内容をこの位置に入れるための記述です。

```html:index.html
<html>
    <?!= HtmlService.createHtmlOutputFromFile("js").getContent(); ?>
  <head>
    <?!= HtmlService.createHtmlOutputFromFile('css').getContent(); ?>
  </head>
  <body>
    <h1>読札アプリ <ruby><rb>月</rb><rp>（</rp><rt>つく</rt><rp>）</rp><rb>読</rb><rp>（</rp><rt>よみ</rt><rp>）</rp></ruby></h1>
    <p>
      <input type="button" onclick="reloadText()" value="  読み札をシャッフルする  ">
    </p>
    <div id='inserthyo'></div>
    <p>
      <a href="#"><input type="button" value="  ページの一番上に戻る  "></a>
    </p>
    <br><br>
    <h1>共有用QR・URL</h1>
    <p>
      <img src="{QRコードのURL}" width="250px" height="250px"/>
    </p>
    <p>
      <input type="button" onclick="copyUrl()" value="  URLをコピーする  ">
    </p>
  </body>
</html>

```

### CSS

タップした札全体に色をつけるために，CSSを以下のようにしています

- `input[type=checkbox]` に対して `display:none;` でチェックボックスを不可視にする
- `td`要素に対して `position:relative;` を設定し，配下の`label`要素（札の文字全体）に対して`display:block;`を設定することで`td`要素内全部をチェックボックスのラベルにする
- `input:checked + label` に対して背景色を設定する


```css:css.html
<style type="text/css">
  body {
    background-color : #E1DAC3;
    font-size        : medium;
    font-family      : "Helvetica Neue", "Helvetica", "Hiragino Sans", "Hiragino Kaku Gothic ProN", "Arial", "Yu Gothic", "Meiryo", sans-serif;
  }
  h1 {
    color            : #333333;
    font-size        : xx-large;
  }
  table {
    border           : none;
    padding          : 0;
  }
  td {
    background-color : #FFFFFF;
    border-color     : #336633;
    border-width     : 0.3rem;
    border-style     : solid;
    border-radius    : 0.3rem;
    width            : auto;
    padding          : 0;
    line-height      : 2rem;
    position         : relative;
  }
  input[type=button] {
    color            : #FFFFFF;
    background-color : #336633;
    border           : none;
    border-radius    : 0.3rem;
    font-size        : medium;
    width            : auto;
    padding          : 0.5rem 1rem;
  }
  input[type=checkbox] {
    display          : none;
  }
  label {
    display          : block;
    padding          : 0.8rem 0.5rem 0.3rem 0.5rem;
  }
  input:checked + label {
    background-color : #CCCCCC;
  }
  iframe {
    border           : 0;
  }
  .num{
    font-size        : 0.8rem;
    position         : absolute;
    right            : 0.5rem;
    bottom           : 0.1rem;
  }
</style>
```

### JavaScript

- 最初にスプシから取ってきたデータをもとに読み札のテーブルを作る関数や，ボタンを押したときの挙動についてここで設定しています

```javascript:js.html
<script type="text/javascript">
  // スプシから読み札の一覧を読み込む
  function loadsheet(){
    google.script.run.withSuccessHandler(outSpreadsheet).generateYomiList();
  }
  window.addEventListener('load', loadsheet);

  // 読み札テーブルの作成
  function outSpreadsheet(spreadsheet_data){
    var htmlTable = '<table id="tableData" border=1>';
    for(var row = 0; row < spreadsheet_data.length; row++){
      htmlTable += '<tr>';
        htmlTable += '<td>' + spreadsheet_data[row][0] + '</td>';
      htmlTable += '</tr>';
    }
    htmlTable += '</table>';
    document.getElementById("inserthyo").innerHTML = htmlTable;
  }

  // 読み札のシャッフル
  function reloadText(){
    let flag = window.confirm("読み札をシャッフルしますが，いいですか？");
    if(flag) {
      google.script.run.reloadSheet();
      update();
    }
  }

  // ページの再読み込み
  function update() {
    google.script.run
      .withSuccessHandler(result).getScriptUrl();
      function result(url){
        window.open(url, '_top');
      }
  }
</script>
```

## アプリを公開する

- GASが完成したら，GASの画面の右上にあるデプロイ→新しいデプロイからデプロイします
    - 種類を「ウェブアプリ」に，アクセスできるユーザーを「全員」に設定してから「デプロイ」します
    - 初回のみ，権限を許可するためのポップアップが出ます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632954/36960977-10e0-1914-4a1e-9cc24d176b85.png)

- デプロイ後，「ウェブアプリ」の下に書かれているURLが公開されたWebアプリのURLです
- 2回目以降のデプロイでは「新しいデプロイ」から行うとURLが変わります
    - URLを変えたくない場合は「デプロイを管理」から編集をクリックし，バージョンを新バージョンとしてデプロイします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632954/9573bf74-a5a5-e3ee-06f8-b6221f06ab5d.png)

# 今後やりたいことなど

競技かるたでは百人一首の100枚のうち，ランダムな50枚を使います。使わないほうの50枚は読み上げるけれど場にない「空札」と呼ばれます。
練習時間が短い場合，全試合で使う札を揃えた上で，空札を何枚か抜いて試合することがあります。どの札を抜くのが良いかいい感じに決めてくれるような機能も実装してみたいと考えています。