LINEとGASを使用して腰痛予防のストレッチを促す


華金タイムが差し迫る金曜午後私はぎっくり腰になってしまいました。
その時「今週末はぎっくり腰を予防するツールを作ろう」と決心しました。

## ツールの概要
1. 毎朝定期で「腰痛予防プログラムをやるか？」的なメッセージがLINEに来る
2. 「OK」と答えたら、指定した時間内で1時間置きにストレッチを促す通知が届く
3. ストレッチをしたらその記録をスプレッドシートに残すことができ、どれくらいストレッチしているのか後からざっくり振り返りができる

という感じのツール



## 動作画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3770353/436c611d-54ff-3ca3-7726-6d955b695ab3.png)




## GASソース
<details><summary>結構長いので折りたたんでいます</summary>

```js
// ↓↓↓↓↓ 自分で書き換えるところ ↓↓↓↓↓　=========================================================================================================
// LINE Bot 設定
const CHANNEL_ACCESS_TOKEN = 'あなたのチャネルアクセストークン'; 
const USER_ID = 'あなたのユーザーID'; // ユーザーIDを設定

// ライブラリを初期化（※GASにライブラリ追加必要　https://github.com/kobanyan/line-bot-sdk-gas）
const lineBot = new LineBotSDK.Client({ channelAccessToken: CHANNEL_ACCESS_TOKEN });

// ストレッチリスト
const stretches = [
  "猫背x鳩胸ストレッチ",
  "足を組み前屈",
  "座ったまま前屈",
  "足を組み身体をひねる",
  "椅子の上で足を抱えるストレッチ",
  "ウォーキング"
];

// 毎朝9時に「今日は計測しますか？」と通知を送信する関数
function sendDailyReminder() {
  try {
    // 既存の計測トリガーを削除
    deleteMeasurementTriggers();
    // 計測アクティブ状態をリセット
    PropertiesService.getScriptProperties().deleteProperty('measurementActive');
    
    lineBot.pushMessage(USER_ID, { type: 'text', text: '今日は腰痛予防プログラムをやりますか？' });
  } catch (e) {
    log_to_sheet("A", `sendDailyReminder Error: ${e.message}`);
  }
}

// 1時間ごとに「腰のケアできてる？」＋ランダムなストレッチ＋「をしてみよう！」とメッセージ送信する関数
function sendHourlyReminder() {
  try {
    const randomStretch = stretches[Math.floor(Math.random() * stretches.length)];
    const message = `腰のケアできてる？ ${randomStretch} をしてみよう！`;
    lineBot.pushMessage(USER_ID, { type: 'text', text: message });
  } catch (e) {
    log_to_sheet("A", `sendHourlyReminder Error: ${e.message}`);
  }
}

// ポストで送られてくるので、ポストデータ取得
function doPost(e) {
  try {
    const json = JSON.parse(e.postData.contents);

    // 返信するためのトークン取得
    const replyToken = json.events[0].replyToken;
    if (typeof replyToken === 'undefined') {
      return ContentService.createTextOutput(JSON.stringify({ 'content': 'No replyToken found' })).setMimeType(ContentService.MimeType.JSON);
    }

    // 送られたメッセージを取得
    const userMessage = json.events[0].message.text;

    // 計測アクティブ状態を取得
    const measurementActive = PropertiesService.getScriptProperties().getProperty('measurementActive') === 'true';

    // 返信メッセージを作成
    let replyMessage = '';

    if (measurementActive) {
      if (userMessage === 'done') {
        recordReply(userMessage);
        replyMessage = { type: 'text', text: '記録しました。' };
      } else if (userMessage === 'END') {
        PropertiesService.getScriptProperties().setProperty('measurementActive', 'false');
        deleteMeasurementTriggers();
        replyMessage = { type: 'text', text: '今日の計測を終了しました。' };
      } else {
        replyMessage = { type: 'text', text: 'doneかENDを入力してください。' };
      }
    } else {
      if (userMessage === 'OK') {
        replyMessage = { type: 'text', text: '計測時間を教えてください（例：10:00-17:00）' };
      } else if (userMessage.match(/^\d{2}:\d{2}-\d{2}:\d{2}$/)) {
        setMeasurementTriggers(userMessage);
        PropertiesService.getScriptProperties().setProperty('measurementActive', 'true');
        replyMessage = { type: 'text', text: `計測時間を${userMessage}に設定しました。` };
      } else {
        replyMessage = { type: 'text', text: 'OKと返すまで待機します' };
      }
    }

    // メッセージを返信
    lineBot.replyMessage(replyToken, replyMessage);

  } catch (e) {
    log_to_sheet("A", `doPost Error: ${e.message}`);
    return ContentService.createTextOutput(JSON.stringify({ 'content': `doPost Error: ${e.message}` })).setMimeType(ContentService.MimeType.JSON);
  }

  return ContentService.createTextOutput(JSON.stringify({ 'content': 'post ok' })).setMimeType(ContentService.MimeType.JSON);
}

// 計測時間帯に合わせてトリガーを設定する関数
function setMeasurementTriggers(timeRange) {
  const [start, end] = timeRange.split('-').map(time => {
    const [hours, minutes] = time.split(':').map(Number);
    const date = new Date();
    date.setHours(hours);
    date.setMinutes(minutes);
    date.setSeconds(0);
    return date;
  });

  for (let d = new Date(start); d < end; d.setHours(d.getHours() + 1)) {
    ScriptApp.newTrigger('sendHourlyReminder')
      .timeBased()
      .at(d)
      .create();
  }
}

// 計測トリガーを削除する関数
function deleteMeasurementTriggers() {
  const triggers = ScriptApp.getProjectTriggers();
  for (let i = 0; i < triggers.length; i++) {
    if (triggers[i].getHandlerFunction() == 'sendHourlyReminder') {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }
}

// 返答をスプレッドシートに記録する関数
function recordReply(reply) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('StretchLog');
  const now = new Date();
  const date = Utilities.formatDate(now, Session.getScriptTimeZone(), 'yyyy/MM/dd');
  const time = Utilities.formatDate(now, Session.getScriptTimeZone(), 'HH:mm:ss');

  sheet.appendRow([date, time, reply]);
}

// 処理の確認用にログを出力する関数
function log_to_sheet(column, text) {
  const logSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('SystemLog');
  let lastRow;
  if (logSheet.getRange(column + "1").getValue() == "") {
    lastRow = 0;
  } else if (logSheet.getRange(column + "2").getValue() == "") {
    lastRow = 1;
  } else {
    lastRow = logSheet.getRange(column + "1").getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow();
    if (lastRow >= 1000) {
      logSheet.getRange(column + "1:" + column + "10").clearContent();
      lastRow = 0;
    }
  }
  const putRange = column + String(lastRow + 1);
  logSheet.getRange(putRange).setValue(text);
}

// 毎朝9時に実行するトリガーの設定
function setupDailyTrigger() {
  const triggers = ScriptApp.getProjectTriggers();
  for (let i = 0; i < triggers.length; i++) {
    if (triggers[i].getHandlerFunction() == 'sendDailyReminder') {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }

  ScriptApp.newTrigger('sendDailyReminder')
    .timeBased()
    .atHour(9)
    .everyDays(1)
    .create();
}


```
</details>

GASにこのコードを貼り付けて、自分のIDとトークンを設定したあとにデプロイしてURLをLINEのWebhook設定の所に入力します。
そのあとにGASの画面でsetupDailyTriggerを選択して実行ボタンを押せば、毎朝届くようになり、設定完了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3770353/b661f3c3-7be4-d66b-bd47-9293330c4471.png)
毎朝通知がくるルーティンを止めたければ、deleteMeasurementTriggersを選択して実行すればOKです。
あと、スプレッドシートで、StretchLogシートとSystemLogシートを作成しておいてください。※名前間違え注意

### 解決していない問題
ちなみにWebhookの設定で【検証】を実行した時は恐らくこちら↓の画面が出てきます。でも今回実現したい動きはできているので放置しています。（エラーの消し方がイマイチ分からなかったのでこのままにしています。すみません。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3770353/5ecaf9a3-c212-4a30-124d-b1ffdc59a60b.png)





## 効果
正直素晴らしいものを作ったと思います。
日頃から良く使うLINEにリマインド通知がくるので確認しやすいですし、リマインド通知がくる時間調整もLINEからできるので管理しやすいです。
今後仕事の日は起動して使っていこうと思います！
さらに使いやすくするために、記録をサマライズして定期的に報告してくれる機能など実装できるベストですが、それは追々やっていきたいなと思います!