当サイトはアフィリエイト広告による収入を得ています

【LINE】シフト希望の回答を催促する(GAS)

2025年7月30日

この前のツイートで、こんな投稿をしました。

前置きとして雑ですが、今日はこの課題に対する対処をしていきたいと思います。

おーくまん
おーくまん

"おーくまん"です。「忖度なし」で、ガジェットレビューをしています。
お問い合わせプライバシーポリシー

▼以前作ったシフト希望のフォームを送信するプログラムはこちら▼

今回の仕様

①スプレッドシートの回答を確認する

  • 設定ファイルにある名前が、スプレッドシートの回答シートにすべて記述されているか確認する
  • 設定ファイルの名前がすべてない場合、②の仕様を実行する

②催促メッセージを送信する

  • 「まだシフト希望の回答をしていない方は回答してください。」とLINEのトークルームに送信する

実装

コードを書く場所

"src"ディレクトリに、新しく「remainde.gas」を作成し、ここにコードを書いていきます。


ホーム
├ config/
│   └ Config                   ← 設定値を管理するスプレッドシート
├ form/                        ← GAS が自動生成する Google フォームを格納
├ sheet/
│   └ shit                    ← フォーム回答が書き込まれるスプレッドシートを格納
└ src/
    ├  main.gs             ← メイン(フォームの送信)
    └ remainde.gs              ← 回答を催促する機能のgasファイル ★新規作成
コード作成場所
ファイル名変更

コード

// 設定を記述したスプレッドシートのファイルID
const CONFIG_SPREADSHEET_ID = 'スプレッドシートのファイルID';
// 設定を管理するシート名
const CONFIG_SHEET_NAME = 'config';

// フォルダ検索フラグ
const ROOT_DIR = 0;
const SPECIFIC_DIR = 1;

/**
 * 未回答者がいる場合、催促のメッセージを送信する
 */
function remindUnanswered() {
  const cfg = loadConfig(); // 既存の設定読込を流用
  const ss = SpreadsheetApp.openById(cfg.spreadsheetId);

  // 回答シートを取得("Form Responses" or "フォームの回答" を検出)
  const responsesSheet = getResponsesSheet_(ss);

  // 回答済みの名前一覧を収集
  let respondedNames = [];
  if (responsesSheet) {
    const lastRow = responsesSheet.getLastRow();
    if (lastRow >= 2) {
      const headers = responsesSheet.getRange(1, 1, 1, responsesSheet.getLastColumn()).getValues()[0];
      const nameCol = findNameColumn_(headers); // 「名前」列の位置
      if (nameCol < 1) {
        // フォームの設問タイトルが「名前」になっていない等、想定外の場合は送信せず終了
        console.log('「名前」列が見つからないため、通知をスキップしました。');
        return;
      }
      respondedNames = responsesSheet
        .getRange(2, nameCol, lastRow - 1, 1)
        .getValues()
        .map(r => String(r[0]).trim())
        .filter(v => v);
    }
  }

  // 未回答者の有無だけ判定
  const respondedSet = new Set(respondedNames);
  const names = (cfg.names || []).map(n => String(n).trim()).filter(Boolean);
  const notAnswered = names.filter(n => !respondedSet.has(n));

  if (notAnswered.length === 0) {
    console.log('未回答者なし:通知をスキップしました。');
    return;
  }

  // 固定メッセージのみ送信(未回答者名は含めない)
  const message = 'まだシフト希望の回答をしていない方は回答してください。';
  sendTextViaLine_(message, cfg);
}

/**
 * config シートから A列(Key)/B列(Value) を読み込み、
 * オブジェクトを返す
 */
function loadConfig() {
  Logger.log("START FUNCTION: loadConfig()");
  const ss    = SpreadsheetApp.openById(CONFIG_SPREADSHEET_ID);
  const sheet = ss.getSheetByName(CONFIG_SHEET_NAME);
  const rows  = sheet.getRange('A2:B' + sheet.getLastRow()).getValues();
  const cfg   = {};
  rows.forEach(function(r) {
    const key = r[0], val = r[1];
    if (key) cfg[key] = val;
  });
  // names はコンマ区切り文字列 → 配列に
  cfg.names = cfg.names ? cfg.names.split(',').map(function(n){ return n.trim(); }) : [];
  Logger.log("cfg: %s", JSON.stringify(cfg));
  return cfg;
}

/**
 * 回答シート(Form Responses / フォームの回答)を返す
 */
function getResponsesSheet_(ss) {
  const sheets = ss.getSheets();
  for (const s of sheets) {
    const n = s.getName();
    if (n.indexOf('Form Responses') !== -1 || n.indexOf('フォームの回答') !== -1) {
      return s;
    }
  }
  return null;
}

/**
 * ヘッダー配列から「名前」列の 1-based インデックスを返す。見つからなければ 0
 */
function findNameColumn_(headers) {
  // 完全一致を優先
  let idx = headers.findIndex(h => String(h).trim() === '名前');
  if (idx >= 0) return idx + 1;

  // 念のため部分一致も最後にチェック(柔軟性確保・任意)
  idx = headers.findIndex(h => String(h).indexOf('名前') !== -1);
  if (idx >= 0) return idx + 1;

  return 0;
}

/**
 * LINEグループにテキストを送信
 */
function sendTextViaLine_(text, cfg) {
  const payload = {
    to: cfg.lineGroupId,
    messages: [{
      type: 'text',
      text
    }]
  };
  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    headers: { Authorization: 'Bearer ' + cfg.accessToken },
    muteHttpExceptions: true
  };
  UrlFetchApp.fetch('https://api.line.me/v2/bot/message/push', options);
}

"スプレッドシートのファイルID"は、以下を参考にconfigファイルのURLから取得してください。

ID取得方法

動作確認

トリガーを設定する

GASエディタ画面から「Triggers」をクリックする。

トリガー設定方法①

「Add Trigger」を押す。

トリガー設定方法②

以下のように設定し、「Save」を押す。

  • Select type of time based trigger:「Week timer(毎週)」にする
  • Select day of week:「Every Friday(毎週金曜日)」にする
  • Select time of day:「5 pm to 6 pm(17時~18時の間)」にする

設定間隔はご自身の都合に合わせて調整してください

動作確認

コードの作成とトリガーを設定して迎えた、金曜日の17:00。例によって全員からの回答はありませんでした。LINEを確認してみると、グループトークに催促のメッセージが届いていました。

LINE画面

おすすめ記事

Galaxy S26 Ultra

スマホ

2026/3/16

【Galaxy S26 Ultra レビュー】さらに日常向けに最適化された最高峰スマホ

僕は毎年Samsungの新作発表会を楽しみにしてたんだけど、ここ数年は新しいスマホが発表されても「どうせAIでしょ」みたいな感じでわくわくすることはなくなってきてた。 ただ、今年は一味違って、AI以外に「プライバシーディスプレイ」なる世界初の機能が搭載されて久しぶりにわくわくしました。1年前にGalaxy S25Ultraを買ったばかりで、ふつうに考えれば買い替える必要はまったくないんだけど、思わず買ってしまったからレビューしていきます。 【Galaxy S26 Ultraの特徴】オールインワンの一言に尽 ...

デスク環境

2026/3/1

【デスク環境が変わる】Ankerの巻き取り式充電ステーションレビュー【迷わず買え】

発売当初はすぐに売り切れた、巻き取り式ケーブル搭載の充電ステーションAnker Nano Charging Station (7-in-1, 100W, 巻取り式 USB-Cケーブル)。 最初は「そんなにいいの?」と思っていたけど、使ってみるとマジでいい。デスク環境が変わるから、ちょっとでも気になっているなら迷わず買ってほしいです。 とはいえ、急に「買え」と言われても「なんで?」となるから実際の使用感を解説していきます。 Anker Nano Charging Station 7-in-1 10 ...

ブログ 雑談

2026/1/22

僕の経歴とブログを始めた経緯

このブログを運営している"なんグラマ"といいます。「なんちゃってプログラマ」の略です。 本業は接客業をしていて、日々お客様と接しています。接客業も好きでこの仕事をしているのですが、他にもいくつか好きなこと(モノ)があります。 それは、 プログラミング ガジェット スイーツ です。 当ブログでは、この3つのジャンルを中心に投稿しています。この投稿では、自己紹介として私の人生の一部を話す中で、ブログを始めた理由も話していきます。 【幼稚園~中学時代】将来の夢は列車の運転士だった 幼稚園くらいの頃から列車が好き ...

オーディオ

2026/1/17

【Soundcore Liberty 5レビュー】2万円以下のコスパ最強イヤホン

2万円以下のコスパイヤホンといえば、これまではAnkerのSoundcore Liberty 4でした。1.5万円という価格ながら日常使いでストレスの少ないまさに"最強"のイヤホンです。 そんなSoundcore Liberty 4に新モデル、Soundcore Liberty 5が登場しました。しかも価格は据え置きで。 実際に使ってみて思ったのは「さすがAnker。」で、性能を向上させつつも無駄を省いていました。それによって前作と同じ1.5万円を維持したのかと思うと感心します。今回はそんなSoundco ...

スマートウォッチ

2026/3/1

【Google Pixel Watch 4レビュー】ふだん使いに最適。健康管理もOKなハイエンドコスパモデル。

この前Galaxy Watch8 Classicを購入したばかりなのに、Google Pixel Watch 4という非常に興味深いスマートウォッチが発売されてしまいました。なんで興味深いかというと、価格は5万円ながら機能がもりもりでコンパクトなスマートウォッチだから(注:5万円は安くない)。 「これは試してみるしかない!」ということで、今回は41mmのサイズを購入して実際に使用してみたレビューをしていきます。 Google Pixel Watch 4のスペック Google Pixel Watch 4の ...

SOUNDPEATS Air5 Pro+

オーディオ

2026/3/3

【SOUNDPEATS Air5 Pro+レビュー】音質とノイキャンに全振りしたコスパイヤホン

「SOUNDPEATS Air5 Pro+の音質とコスパがいい。」そんなSNS投稿を見たときに1度はスルーしたんだけど、結局買ってしまった。なぜならたくさんの人がおすすめしてて、何度も僕のSNSに出てくるから。 これ以上イヤホンが必要ないことは分かっているけど、何度も表示されると気になってしょうがなくなってしまったんです。実際に使ってみると、たしかに金額以上の音質があってコスパは良さそうだったから僕も紹介します。 【結論】音質とノイキャンとコスパを求めるならこのイヤホン まず結論は「音質とノイキャン、そし ...

  • この記事を書いた人
アイコン

おーくまん

福岡在住の24歳。ガジェット好きで、日々の暮らしをアップデートするための情報をお届けします。ときどきプログラムも投稿します。

この記事もおすすめ

1

発売当初はすぐに売り切れた、巻き取り式ケーブル搭載の充電ステーションAnker Nano Charging Station (7-in-1, 100W, 巻取り式 USB-Cケーブル)。 最 ...

Galaxy Watch8 Classic 2

僕はこれまでGalaxy Watch4 Classicを4年使用していています。ただ、さすがに4年も経つとバッテリーの減りが早くなったり、動作がもっさりしてきたりと不満を感じるようになってきました。そ ...

SOUNDPEATS Air5 Pro+ 3

「SOUNDPEATS Air5 Pro+の音質とコスパがいい。」そんなSNS投稿を見たときに1度はスルーしたんだけど、結局買ってしまった。なぜならたくさんの人がおすすめしてて、何度も僕のSNSに出て ...

-プログラム
-,