お医者さんとコラボして、医院で使う順番待ち発券システムを開発している話

この記事は[プロトアウトスタジオ Advent Calendar 2020](https://qiita.com/advent-calendar/2020/protoout)の 8 日目の記事です。

プロトアウトスタジオ 2 期生同期のお医者さん @doikatsuyuki さんとコラボして、
医院で使用する順番待ち発券システムを一緒に開発した話です。

7 日目は、@tatsuya1970 さんの「[M5Stack + Unity 『草刈りゲーム』の作り方](https://qiita.com/tatsuya1970/items/d230fd374e7dbcfb58a0)」でした。

**クラウドファンディング挑戦中！　**とのことなので、興味がある方はぜひ覗いてみてください。
→[ハッカソンから生まれたアイデア『草刈りゲーム』のeスポーツイベントを開催したい](https://camp-fire.jp/projects/view/325175)

##  欲しい物は自分で開発する時代

ある日、プロトアウトスタジオ同期のお医者さん @doikatsuyuki さんから自身の医院で使う順番待ち発券システムを一緒に開発しないか、という話をもらいました。

普通、システム開発というと、**エンジニアが作ったものをお客さんへ納品する**というイメージですが、
今回はそうではなく、**お医者さんが自分の手で開発を行い、機能追加も運用も自分で行えるようにしよう**というところがポイントでした。

そんな中、僕はエンジニアとして、技術選定とサンプルコード作成、ハンズオン、機能追加のフォローなどを行っています。

## 技術選定のポイントは「自分で管理できること」

今回、以下の技術を採用しました。

- バックエンド：**Google App Script**
- DB：**Googleスプレッドシート**（DB じゃないけど永続化できてるということで）
- フロントエンド：**LINE Bot**

システム構成はこんな感じです。

[![Image from Gyazo](https://i.gyazo.com/6f6930f556b62da31f03be772c99f91e.png)](https://gyazo.com/6f6930f556b62da31f03be772c99f91e)
この中で一番悩んだのが DB です。

本当は、Firebase の Realtime Database や AWS DynamoDB などの NoSQL DB を使えるとエンジニア的には楽です。

しかし、エラーが発生した場合、お医者さん自身が調査を行い、また復旧作業も行うことを想定しないといけません。
Google スプレッドシートであれば、データに不整合が発生したとしても Web の UI から簡単に修正できます。
修正の容易さが採用の決め手となりました。

バックエンドとフロントエンドの採用理由については、以下の通りです。

- バックエンド：Google App Script は、スプレッドシートのセル操作を簡単に行うことができ、また JavaScript ライクな言語なので、プロトアウトスタジオの授業の内容でおおよそカバーできます。
- フロントエンド：LINE Bot も授業の内容で問題なく開発でき、かつ、ユーザーが QR コードを読み込むだけで利用を始められ、ユーザーのインストールの手間も減らすことができます。

あと、環境構築がほとんど不要で、無料で利用できるという点もポイントが高かったです。

## 最初はハンズオンで勢いをつける→自身で機能追加

実際の開発フェーズでは、僕がサンプルソースを用意し、動くところまで一緒にハンズオンを行いました。

手順はこちらの記事にまとまっています。
→ [1時間で出来る　LINE×GASで順番取り予約システムの作成/@doikatsuyuki](https://qiita.com/doikatsuyuki/items/842b675415abd463e499) 

その際、使用したソースコードがこちらです。

```javascript:
// LINE developersのメッセージ送受信設定に記載のアクセストークン
var ACCESS_TOKEN = 'LINE=ACCESS_TOKEN_HERE';

function doPost(e) {
  // WebHookで受信した応答用Token
  var replyToken = JSON.parse(e.postData.contents).events[0].replyToken;
  // ユーザーのメッセージを取得
  var userMessage = JSON.parse(e.postData.contents).events[0].message.text;
  // 応答メッセージ用のAPI URL
  var url = 'https://api.line.me/v2/bot/message/reply';
  
  var text = '';
  if (userMessage === '発券する') {
    text = getNumber() + '番目です。';
  } else if (userMessage === '何番目？') {
    text = '〇〇番目を診察中です';
  }

  UrlFetchApp.fetch(url, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': replyToken,
      'messages': [{
        'type': 'text',
        'text': ,
      }],
    }),
    });
  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}

function getNumber() {
  //1. 現在のスプレッドシートを取得
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  //2. 現在のシートを取得
  var sheet = spreadsheet.getActiveSheet();
  //3. 指定するセルの範囲（A1）を取得
  var range = sheet.getRange("A1");
  //4. 値を取得する
  var value = range.getValue();
  //5. 発券済み枚数を増やす
  range.setValue(value + 1);
 
  //ログに出力
  return value;
}
```

こちらのサンプルと、[Google Apps ScriptでLINE BOTつくったら30分で動かせた件](https://qiita.com/hakshu/items/55c2584cf82718f47464)を参考に、1～2 時間ほどハンズオンを行い、LINE Bot が動く状態にしました。

そこから、@doikatsuyuki さんがメッセージの内容を更新したり、診察中番号の通知機能などが追加を行ったりしています。

```javascript
if (userMessage === '発券') {
  text = 'LINE順番予約：予約番号は【'+ getNumber() + '】※受付で番号をお伝えください';
} else if (userMessage === '今何番？') {
  text = '現在【'+ getNumberB1() + '番】を診察中です。※遅れた場合キャンセル扱いとなります';
} else if (userMessage === '現状') {
  text = '【'+ getNumberA1() + '番】まで発券済、【' + getNumberB1()　+ '番】を診察中、【'　+ getTime() + '分】の待ち時間　※待ち時間15分以上でご予約下さい　遅れるとキャンセルされます';
} else if (userMessage === 'ん') {
  incrementB1();
  text = getNumberB1()+ '番を診察中に更新しました。';
}else if (userMessage === '利用時間') {
 text =  '午前8:30～11:00　午後2:00～4:30';
}
```

## できたコードがこちら

```javascript
// LINE developersのメッセージ送受信設定に記載のアクセストークン
var ACCESS_TOKEN = 'LINE=ACCESS_TOKEN_HERE';

function doPost(e) {
  var event = JSON.parse(e.postData.contents).events[0];
  Logger.log(e);
  SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange("A10").setValue(JSON.stringify(event));
  // WebHookで受信した応答用Token
  var replyToken = event.replyToken;
  // ユーザーのメッセージを取得
  var userMessage = event.message.text;
  // 応答メッセージ用のAPI URL
  var url = "https://api.line.me/v2/bot/message/reply";
  // 応答メッセージの内容
  var messages = [
    {
      type: "text",
      text: "",
    },
  ];

  // 必要に応じて、ここのif文を追加していってください。
  if (userMessage === '発券') {
    messages[0].text = '順番予約：予約番号は【'+ getNumber() + '】※受付で番号をお伝えください';
  } else if (userMessage === '今何番？') {
    messages[0].text = '現在【'+ getNumberB1() + '番】を診察中です。※遅れた場合キャンセル扱いとなります';
  } else if (userMessage === '現状') {
    messages[0].text = '【'+ getNumberA1() + '番】まで発券済、【' + getNumberB1()　+ '番】を診察中です。';
  } else if (userMessage === '利用時間') {
    messages[0].text =  '午前8:30～11:00　午後2:00～4:30';
  } else {
    messages[0].text = 'サポートされていないメッセージです。';
  }

  UrlFetchApp.fetch(url, {
    headers: {
      "Content-Type": "application/json; charset=UTF-8",
      Authorization: "Bearer " + ACCESS_TOKEN,
    },
    method: "post",
    payload: JSON.stringify({
      replyToken: replyToken,
      messages: messages,
    }),
  });
  return ContentService.createTextOutput(
    JSON.stringify({ content: "post ok" })
  ).setMimeType(ContentService.MimeType.JSON);
}

//セルA1（発券済み番号）を１増やしていく関数
function getNumber() {
  //1. 現在のスプレッドシートを取得
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  //2. 現在のシートを取得
  var sheet = spreadsheet.getActiveSheet();
  //3. 指定するセルの範囲（A1）を取得
  var range = sheet.getRange("A1");
  //4. 値を取得する
  var value = range.getValue();
  //5. 発券済み枚数を増やす
  range.setValue(value + 1);
  //ログに出力
  return value;
}

//セルA1値-１（発券済み番号を取得する関数）
function getNumberA1() {
  //1. 現在のスプレッドシートを取得
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  //2. 現在のシートを取得
  var sheet = spreadsheet.getActiveSheet();
  //3. 指定するセルの範囲（A1）を取得
  var range = sheet.getRange("A1");
  //4. 値を取得する
  var value = range.getValue() - 1;
  //ログに出力
  return value;
}

//セルB1値（診察中番号）を取得する関数
function getNumberB1() {
  //1. 現在のスプレッドシートを取得
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  //2. 現在のシートを取得
  var sheet = spreadsheet.getActiveSheet();
  //3. 指定するセルの範囲（B1）を取得
  var range = sheet.getRange("B1");
  //4. 値を取得する
  var value = range.getValue();
  //ログに出力
  return value;
}
```

## さらに

最近では、診察中の番号を iPad に大きく表示するアプリ（こちらは Vue.js を使った Web アプリ）も作成しました。
数秒に一度、診察中の番号を自動で更新します。

作成手順はこちら。
→ [診察中番号表示WEBアプリの作成/@doikatsuyuki](https://qiita.com/doikatsuyuki/items/2fa437eabbf778782bc7)

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90087/4ce73c3c-d961-bf90-2a11-b9940e8a4551.png" width="600px" />

## まとめ

プロトアウトスタジオ卒業生のコラボの一例を紹介しました。
他にもコラボ企画を進めているので、また記事を書こうと思います。