【LINE】Messaging API＋スプレッドシートで同棲生活の家計計算を自動化

## 背景
彼女と同棲を始めるにあたり、それぞれが買い物をした際の生活費の管理方法を検討していました。

クレジットカードの共有や家族カードの発行も検討しましたが、まだ結婚していないため選択肢がほとんどありませんでした。
ならば、「連絡手段として普段使っているLINEで管理しよう」と思い、LINE botを作りました。

## やったこと・やれること
- 私と彼女がそれぞれ買い物した際の生活費（食費や日用品など）に対して、どちらがいくら多く払ったか（精算状況）を管理するデータベース（スプレッドシート）を作成
- 買い物の金額をLINEにメッセージすると、スプレッドシートにデータ追加＆現在の精算状況をLINE botが教えてくれる
- メッセージした金額に対し、払った月（＝入力した月）が自動でデータ追加される
- カテゴリ（食費や日用品など）を追加で入力することで、選択したカテゴリの今月の支出額を表示できる

## 画面イメージ
**1. 金額を入力すると、精算状況をLINE botが返答します**
![IMG_1640.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2656606/1ea7d1db-01d0-bb1a-be66-ea9541a37326.png)
**2. 続けてカテゴリ入力を促されるので、カテゴリを選択します**
![IMG_164000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2656606/9728de2c-5460-c12d-1769-2fe1244397dc.png)
**3. 選択したカテゴリの今月の支出額をLINE botが返答します**
![IMG_1641.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2656606/bbb0dbcb-3096-0ee8-5e5b-52c21e065c82.png)
**4. 「今月の家計簿」と入力すると、カテゴリ毎の今月の支出額をLINE botが返答します**
![LINE_capture_673801742.984483.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2656606/bb5b3852-810a-a14a-cbb0-0081a4faa8a6.jpeg)
**5. 「〇〇の家計簿」のように記載すると、カテゴリ毎の指定した月の支出額をLINE botが返答します**
![LINE_capture_673801698.996716.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2656606/8cc371ca-f687-e975-cc87-cb455fc86647.jpeg)

## 詳細
### システム構成
簡単な図ですが、下記の構成で各種連携をしています。
![LINEsystem.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2656606/f7d08678-73d8-c0b5-a451-f0d3c3912ef8.png)

### 事前準備
**1. LINEグループ作成**
私・彼女・LINE botの3人のグループを作成しました。このグループにメッセージを送ると、LINE botが返答します。

**2. スプレッドシート準備**
スプレッドシートは以下の4シートを用意しました。

- **「ユーザ情報」シート**：ユーザIDと氏名を紐づける
![スクリーンショット 2022-06-19 16.50.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2656606/f3de833e-877b-7930-012e-ee53f6b1846b.png)
- **「\<ユーザID>」シート**：ユーザID毎にシートを作成し、日付・カテゴリ・金額を管理する
![スクリーンショット 2022-06-19 16.51.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2656606/62e0fd15-e8eb-c26f-8e44-50b1cd53357d.png)
- **「集計」シート**：どちらがいくら多く払ったかを計算・表示する
![スクリーンショット 2022-06-19 16.51.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2656606/e952f7cd-3a85-15f3-5192-98357a81cf49.png)
- **「カテゴリ」シート**：カテゴリ×月毎の家計簿を計算・表示する
![スクリーンショット 2022-06-19 16.50.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2656606/b025dc50-cbf5-cfc8-2435-798729f3198f.png)

**3. Messaging API利用登録＆スプレッドシート連携**
LINE Messaging APIを利用するための登録を行います。また、WebhookでGASにリクエストを送るための設定します。
参考になる記事がたくさんあるのでここでは割愛します。

### 動作とコーディング
**1. メッセージ受け取り**
メッセージを受け取った際、次の4パターンに分岐します。

A. **金額が入力された場合**：B,C,D以外
B. **カテゴリが入力された場合**：イベントタイプがポストバックの場合
C. **「〇〇の家計簿」と入力された場合**：正規表現で判断
D. **想定外の入力値の場合**：B,C以外で、かつ値が数値ではない場合

:::note 
インフォメーション
カテゴリは選択形式で入力を促したかったので、ポストバックイベントの[クイックリプライ](https://developers.line.biz/ja/reference/messaging-api/#quick-reply-button-object)を使用しました。
そのため、入力がカテゴリかどうかは、イベントタイプがポストバックかどうかで判断できます。
:::

```javascript
// LINEからのメッセージ受け取り
function doPost(e) {
  // 受け取ったメッセージの前処理
  // 取得したデータをjsonにparse
  var json = JSON.parse(e.postData.contents);
  // user_idを取得
  user_id = json.events[0].source.userId;
  // reply_tokenを取得
  reply_token = json.events[0].replyToken;
  // eventタイプを取得
  type = json.events[0].type;
  if(type=='postback'){
    message = json.events[0].postback.data;
  }else{
    // messageを取得
    message = json.events[0].message.text;
  }
  // 月の家計簿の取得判定
  month_tmp = message.match(/(20\d{2}年[1-9]月|20\d{2}年1[0-2]月|先月|今月)の家計簿/);

  // 受け取ったメッセージのタイプ確認
  if(type=='postback'){
    setCategory(user_id, message)
    value = getCatgoryMoney(message);
    text = "今月の" + message + "は" + value + "円です。";
    reply(text, reply_token);
  }else if(month_tmp){
    month = setMonth(month_tmp[1]);
    text = getMonthMoney(month);
      reply(text, reply_token);
  }else if(isNaN(message)){
    // 受け取ったメッセージのフォーマット確認（数字以外はエラー）
    text = "金額は数値のみで入力してください。\n例）\n〇：3000\n×：3000円\n×：3千円\n\n";
    text = text + "家計簿の表示は下記のフォーマットで入力してください。\n例）\n〇：今月の家計簿\n〇：先月の家計簿\n〇：2022年4月の家計簿\n×：4月の家計簿";
    reply(text, reply_token);
  }else{
    // 受け取った金額をDBに追加
    setMoney(user_id, message)
    // 家計を計算
    var {user_name, value} = calcMoney()
    if(value==0){
      text = "金額清算は不要です。";
    }else{
      text = "現在、" + user_name + "が" + value + "円多く支払っています。";
    }
    text = text + "\n\n続けて、カテゴリ登録をしてください。"
    reply_category(text, reply_token);
  }
}
```

**2. 金額が入力された場合の処理**
金額が入力された場合の処理は、下記のように続きます。

A. 入力された金額をスプレッドシートに追加
B. 精算金額をスプレッドシートから取得
C. 精算金額と、カテゴリ入力を促すメッセージを返信

```javascript
// 受け取った金額をDBに追加
function setMoney(user_id, message) {
  // ユーザIDに対するシートを取得
  var data = SpreadsheetApp.openById(sheet_id).getSheetByName(user_id);
  // カラム番号の最終行を取得
  var last_row = data.getRange(1, 3).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow();

  // 最終行の1行下に金額を追加
  data.getRange(last_row+1, 3).setValue(message);
  // 最終行の1行下に本日の日付を追加  
  data.getRange(last_row+1, 1).setValue(today);
}
```

```javascript
// 家計を計算
function calcMoney() {
  // ユーザIDと金額を取得
  var data = SpreadsheetApp.openById(sheet_id).getSheetByName("集計");
  user_id = data.getRange('A6').getValue();
  value = data.getRange('B6').getValue();

  // ユーザIDをユーザ名に変換
  if(user_id=='-'){
    user_name = '-';
  }else{
    user_name = getUsername(user_id)
  }

  return {user_name, value}
}

// ユーザIDをユーザ名に変換
function getUsername() {
  var data = SpreadsheetApp.openById(sheet_id).getSheetByName("ユーザ情報");
  for(var i=2; i<=data.getLastRow(); i++){
    if(data.getRange(i,1).getValue()==user_id){
      user_name = data.getRange(i,2).getValue();
      break
    }
  }
}
```

```javascript
function reply_category(text, reply_token) {
  var url = "https://api.line.me/v2/bot/message/reply";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };
 
  var postData = {
      "replyToken" : reply_token,
      "messages" : [
        {
          'type':'text',
          'text':text,
          'quickReply': {
            'items': [
              {
              'type': 'action',
              'action': {
                'type': 'postback',
                'label': '食費',
                'displayText': '食費',
                'data': '食費'
                }
              },
              {
              'type': 'action',
              'action': {
                'type': 'postback',
                'label': '日用品',
                'displayText': '日用品',
                'data': '日用品'
              }
              },
              {
              'type': 'action',
              'action': {
                'type': 'postback',
                'label': '家電',
                'displayText': '家電',
                'data': '家電'
              }
              },
              {
              'type': 'action',
              'action': {
                'type': 'postback',
                'label': '家具',
                'displayText': '家具',
                'data': '家具'
              }
              },
              {
              'type': 'action',
              'action': {
                'type': 'postback',
                'label': 'その他',
                'displayText': 'その他',
                'data': 'その他'
              }
              }
            ]
          }
        }
      ]
    };
 
  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };
 
  return UrlFetchApp.fetch(url, options);
}
```

:::note 
インフォメーション
カテゴリは現状ハードコーディングになっていて、カテゴリを追加したい場合に都度スクリプトを触る必要があります。
スプレッドシートに追加すると自動連携される機能など、改善案を模索中です。
:::

**3. カテゴリが入力された場合の処理**
カテゴリが入力された場合の処理は、下記のように続きます。

A. 直前に入力された金額に対して、選択したカテゴリをスプレッドシートに追加
B. 選択したカテゴリの今月の支出額をスプレッドシートから取得
C. 支出額をメッセージで返信

```javascript
// カテゴリを登録
function setCategory(user_id, message) {
  // ユーザIDに対するシートを取得
  var data = SpreadsheetApp.openById(sheet_id).getSheetByName(user_id);
  // カラム番号の最終行を取得
  var last_row = data.getRange(1, 3).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow();

  // 最終行に金額を追加
  data.getRange(last_row, 2).setValue(message);
}
```

```javascript
// カテゴリ毎の金額を取得
function getCatgoryMoney(message) {
  // カテゴリシートを取得
  var data = SpreadsheetApp.openById(sheet_id).getSheetByName("カテゴリ");
  // カテゴリの行を取得
  var cate_row = 0
  for(var i=2; i<=data.getLastRow(); i++){
    if(data.getRange(i,1).getValue()==message){
      cate_row = i;
      break
    }
  }
  // 日付に対する今月のカラムを表示
  var value = "undefined"
  for(var i=2; i<=data.getLastColumn(); i++){
    if(Utilities.formatDate(data.getRange(1,i).getValue(), 'Asia/Tokyo', 'yyyy/MM/dd')>today){
      value = data.getRange(cate_row,i-1).getValue();
      break
    }
  }
  return value
}
```

```javascript
// メッセージを送信
function reply(text, reply_token) {
  var url = "https://api.line.me/v2/bot/message/reply";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };
 
  var postData = {
      "replyToken" : reply_token,
      "messages" : [
        {
          'type':'text',
          'text':text,
        }
      ]
    };
 
  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };
 
  return UrlFetchApp.fetch(url, options);
}
```

**4. 「〇〇の家計簿」と入力された場合の処理**
「〇〇の家計簿」と入力された場合の処理は、下記のように続きます。

A. 文字データを日付データに変換
B. 指定した月の家計簿（カテゴリ毎の支出額）をスプレッドシートから取得
C. 指定した月の家計簿をメッセージで返信　※2の返信する場合と同じ関数を使用します

```javascript
// 月の情報からその月の日付を取得
function setMonth(month_tmp){
  if(month_tmp=="今月"){
    month = today;
  }else if(month_tmp=="先月"){
    month = last_month;
  }else{
    month_tmp2 = month_tmp.match(/(\d+)年(\d+)月/);
    date.setFullYear(month_tmp2[1])
    date.setMonth(month_tmp2[2]-1);
    month = Utilities.formatDate(date, 'Asia/Tokyo', 'yyyy/MM/dd');
  }
  return month
}
```

```javascript
// 今月の家計簿を計算
function getMonthMoney(month){
  // カテゴリシートを取得
  var data = SpreadsheetApp.openById(sheet_id).getSheetByName("カテゴリ");
  // 日付に対する指定した月のカラムを表示
  var month_column = 0;
  for(var i=2; i<=data.getLastColumn(); i++){
    if(Utilities.formatDate(data.getRange(1,i).getValue(), 'Asia/Tokyo', 'yyyy/MM/dd')>month){
      month_column = i-1
      break
    }
  }
  if(month_column<=1){
    text = "指定した月のデータがありません。";
    return text
  }
  // 指定した月の家計簿を取得
  text = "";
  for(var i=2; i<=data.getLastRow(); i++){
    if(data.getRange(i,1).getValue()=="合計"){
      text = text + "\n" + data.getRange(i,1).getValue() + ": " + data.getRange(i,month_column).getValue() + "円";
      break
    }else{
      if(data.getRange(i,1).getValue()==""){
        category = "未分類";
      }else{
        category = data.getRange(i,1).getValue()
      }
      text = text + category + ": " + data.getRange(i,month_column).getValue() + "円\n";
    }
  }
  return text
}
```

## 感想
### よかったこと
- LINEにメッセージ送るだけなので、彼女の導入障壁を低くすることができた（彼女も使いやすいしわかりやすいと言ってくれました）
- 自動的に家計管理がされ、二人で家計について話す機会が増えた

### 今後改善したいこと
- LINEへ入力するひと手間が面倒：具体案はありませんが、何とかなるべく手間を減らす方法を模索中です

### 総評
「LINE botを使ってみたいけれどやりたいことが浮かばない...」と思っていたのでちょうどよい機会になりました。

付き合って3ヶ月で同棲開始と割とスピード感があり、互いの知らない面が多々ある状態だったので、「少しプログラミングができる頼れる人」と思って貰えた気がしてそれもよかったです。

**「LINE botはカップルの絆を深める」かもしれません。**