はじめに
Merry Christmas!クリスマスイブですね。
NRI xPalette Advent Calendar 2025もいよいよ残り2日となりました。
本日24日目は、
GAS(Google Apps Script)とLINE Messaging APIを活用した推し活リマインドBotの作成についての記事をお届けします。
本記事を書くきっかけは、外部サービスを組み合わせて1つのサービスを1から作りたいという筆者の好奇心です。数ある技術の中でも、GASとLINE Messaging APIは導入ハードルが低く、手軽に試せる点が魅力的でした。
そこで本記事は、LINE Botを作成する際にGASとLINEを連携させる一連の流れを整理し、躓きやすいポイントをピックアップして記録に残すことに焦点を当てて執筆しています。
また、近年は生成AIの活用が急速に広がっています。
そこで本記事では、Googleが提供する生成AI Geminiを活用し、VSCodeのような対話型IDEがGASでも実現できるのかという点についても検証しています。
想定している読者
本記事は以下のような人を読者として想定しています。
- プログラミング初心者だけど何か動くものを作ってみたい
→ サーバー構築や本格的なバックエンド知識が不要なので初心者でも取りかかりやすい - LINE Botに興味はあるが作成手順がわからない
- 生成AI(Gemini) × 開発に興味がある
GASとは
GASは、Googleドライブを活用して動作するクラウドベースのJavaScriptプラットフォームです。Googleが提供する各種サービス(スプレッドシート、Gmail、カレンダー、Google Driveなど)とシームレスに連携でき、業務フローの統合・自動化を手軽に実現できます。
GASには以下のような機能があるため、LINE Bot/スプレッドシートと手軽に連携できます。
- 特別な認証設定やサーバー構築が不要で、Googleのサービスと即時連携できる
- 外部APIを呼び出すことができる
- GASのURLを発行でき、Webhookを通じてGASからユーザーに自動通知できる
(参考:Apps Script | Google for Developers)
GASはGoogle Workspaceもしくはスプレッドシートの拡張機能→Apps Scriptで開くことができます。
コード.gsにAPIのPOST処理を書くことでAPIの実行ができるので、自分が作成した好きな機能をLINE Bot経由で呼び出すことができます。
本記事で分かること
本記事の目的
- GASやLINE Messaging APIの使い方の理解を深める
- LINE Botの作成手順を知る
- GeminiによるGAS生成の可能性をみる
最終的なアーキテクチャ図
1章 LINE Botアカウント作成 & Messaging APIの有効化
2章 Googleスプレッドシートの準備
3章 GASの実装
4章 トリガー設定
5章 デプロイ・Webhook設定
6章 テスト
7章 Geminiとの連携
実装した機能
今回は既存のリマインダーではライブ申込締切や当落日、結成日のような記念日など推し活をリマインド・記録するのに物足りないと感じた筆者が、GASとLINEを使って理想のリマインダーを作るべく、以下の機能を実装しました。
機能一覧
- イベント操作振り分け処理(Webhook受信処理)
- LINEトーク画面における機能
- イベント登録
- ID付与による推し活数記録
- イベント削除
- イベント一覧取得
- ヘルプメッセージ表示
- イベント登録
- 日次および年次リマインド判定処理
- LINEメッセージ送信処理(Reply & Push)
以降の章で、作成手順や詳細設定について説明していきます。
1. LINE Botアカウント作成 & Messaging APIの有効化
まずは準備として、LINE Botアカウントの作成とMessaging APIチャネルの作成を行います。
1.1 LINE Botアカウントの役割
LINE Botアカウントは、友だち追加してくれたユーザーに対して情報を発信できる仕組みで、主に企業や店舗が顧客と直接つながるために利用されています。
一方で、アカウントを外部に公開しなければ、自分専用のBotとして利用も可能です。
今回は後者の用途を想定し、自分用の推し活リマインドBotを作成していきます。
1.2 LINE Messaging APIの役割
LINE Messaging APIを利用することで、Botサーバー(今回は GAS)がLINEプラットフォームとデータのやり取りを行えるようになります。
このMessaging APIがないと、GAS側で実装した処理とLINE Botを連携させることができないため、LINE Botを作成するうえで非常に重要な要素です。Messaging APIを有効化することで、LINE Botアカウントを通じて、ユーザーからのメッセージを受け取り、返信を行うことができるようになります。
(参考:Messaging APIの概要)
1.3 設定手順
1.3.1 LINE Developersにログイン
- ブラウザで https://developers.line.biz/ を開く
- 右上の「コンソール」をクリック
- LINEアカウントでログイン
1.3.2 Provider(プロバイダー)を作る
プロバイダーとは、チャネルにアプリを提供する個人または組織のことなので、チャネルをグループ化するための箱のようなものとイメージできていれば大丈夫です。
1.3.3 Messaging APIチャネルを作る
- 作ったプロバイダーを選択し、チャネル設定→Messaging APIを選択
- LINE公式アカウントを作成→フォームに必要事項を入力し→規約に同意して作成
作成が完了すると、チャネルシークレットなどが画面に表示されます。
あとでGASに貼るため、この画面で出るチャネルシークレットをコピーして安全に保存しておいてください。
セキュリティ注意
シークレットトークンは他者への共有NGです。
SNSや公開リポジトリなどへ貼らないようにしてください。
もしもチャネルシークレットが出てこなかったり、保存し忘れてしまった場合は、
LINE Developers→対象プロバイダー選択→チャネル基本設定
1.3.4 チャネルアクセストークン(長期トークン)を発行する
- LINE DevelopersのMessaging API設定ページでチャネルアクセストークンセクションを探す
- 「発行」ボタンを押して チャネルアクセストークンを作る
- 発行されたチャネルアクセストークン(長い英数字列)をコピーして安全に保存
セキュリティ注意
シークレットトークンと同様、チャネルアクセストークンは他者への共有NGです。
SNSや公開リポジトリなどへ貼らないようにしてください。
1.3.5 Botアカウントを友だちに追加する
1.3.6 Replyの設定
重要
Botと管理画面の自動返信がぶつかって、二重返信や誤動作が起きてしまう可能性があるため、チャットは必ずOFFにしてください。(あいさつメッセージは友だち追加時のみのため、ON/OFFどちらでも可)
2. Googleスプレッドシートの準備
- IDを1から順に付与することで推し活の総数を記録したい
- LINEトーク上でイベントの名前や日付、URL情報を登録・削除できるようにしたい
- 日付に年を含む場合は単発イベント、月日のみの場合は毎年通知するようにしたい
- 通知は1週間前、前日、当日の3回行いたい
といった筆者の希望があったため、今回は以下のようなスプレッドシートの構成が最適だと判断しました。
また、シート名はGASと連携させる際に必要となるものなので、Botに関連のある名前を付けるといいかもしれません。
3. GASの実装
ここから実際にGASの実装に入ります。
以下の実装コードはGASのコード.gsに書いてください。
3.1で「コア機能」を初歩の基本機能とし、3.2で「利便性や自動化を高めるもの」を追加機能として分けて実装します。
3.1 コア機能の実装
まずは「LINEで送ったものが保存され、確認できる」という データベースの基本(CRUD) を実装します。
| # | 実装した関数 | 役割 |
|---|---|---|
| ① | doPost(e) | LINEからの命令を「登録」か「一覧」かに振り分ける |
| ② | registerEvent(text) | シートにデータを書き込む |
| ③ | listEvents() | シートの中身を全部取り出して表示する |
| ④ | replyText(token, msg) | LINEの「返信(Reply)」の仕組みを構築する |
この段階でできること
- LINEで「登録 ライブ 05/10」などと送ると、スプレッドシートに記録
- 「一覧」と送ると、今までの登録内容が返ってくる
プログラム詳細
// ===== 設定 =====
const CHANNEL_ACCESS_TOKEN = "自分のアクセストークン";
const SHEET_NAME = "シート名";
// ===== ①Webhook受信 =====
function doPost(e) {
const event = JSON.parse(e.postData.contents).events[0];
const msg = event.message.text.trim();
const replyToken = event.replyToken;
// 命令によって処理を振り分ける
if (msg.startsWith("登録")) {
replyText(replyToken, registerEvent(msg));
} else if (msg === "一覧") {
replyText(replyToken, listEvents());
} else {
replyText(replyToken, "「登録 イベント名 日付 (URL)」か「一覧」と送ってね!");
}
}
// ===== ②イベント登録(書き込み担当)=====
function registerEvent(text) {
try {
let parts = text.split(/[\s ]+/);
if (parts.length < 3) return "✖形式が違うよ\n例)登録 推し誕 03/15 URL(なくてもOK)";
let name = parts[1];
let dateStr = parts[2];
let url = parts[3] || "";
let sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
// IDの自動採番(既存の最大ID+1)
let values = sheet.getDataRange().getValues();
let id = values.length; // 簡易的に行数でID作成
sheet.appendRow([id, name, dateStr, url]); // スプレッドシートに追記
return `登録したよ!\nID:${id}\n${name}\n${dateStr}`;
} catch (e) {
return "登録に失敗しました。";
}
}
// ===== ③一覧(読み込み担当)=====
function listEvents() {
let sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
let values = sheet.getDataRange().getValues();
if (values.length < 2) return "まだ何も登録されていないよ!";
let msg = "📅登録イベント一覧\n";
for (let i = 1; i < values.length; i++) {
const r = values[i];
msg += `ID:${r[0]} ${r[1]} ${r[2]}\n`;
}
return msg;
}
// ===== ④LINE送信(返信担当)=====
function replyText(token, message) {
UrlFetchApp.fetch("https://api.line.me/v2/bot/message/reply", {
"method": "post",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN
},
"payload": JSON.stringify({
"replyToken": token,
"messages": [{ "type": "text", "text": message }]
})
});
}
3.2 運用機能と自動通知の追加
追加機能として、登録したものを消したり、使い方がわからなくなった時のためのサポート機能や、推し活リマインドBotの最大の目的である自動通知機能を実装します。
| # | 実装した関数 | 役割 |
|---|---|---|
| ⑤ | ymd(d) | 日付の表示形式を整える |
| ⑥ | deleteEvent(text) | 指定したIDの行(登録したイベント)を削除する |
| ⑦ | helpMessage() | ユーザーにコマンドの書き方を教える |
| ⑧ | notifyEvents() | 日付を計算し、今日がイベント日かどうか判定する |
| ⑨ | pushText(msg) | 返信ではなく、Bot側から一方的に送る「プッシュ通知」の仕組みを構築する |
この段階でできること
- 間違えて登録したものを削除できる
- 使い方がわからなくても「help」で解決できる
- LINEを開かなくても、1週間前や当日に勝手に通知が来るようになる
- 「通知済みフラグ(スプレッドシートF列)」の管理により、二重通知を防ぐ
プログラム詳細(最終版)
// ===== 設定(追加部分のみ) =====
const CHANNEL_ACCESS_TOKEN = "自分のアクセストークン";
const SHEET_NAME = "シート名";
const PUSH_USER_ID = "自分のユーザーID"; // チャネル基本設定より確認できる
// ===== ①Webhook受信(変更反映後)=====
function doPost(e) {
const event = JSON.parse(e.postData.contents).events[0];
const msg = event.message.text.trim();
const replyToken = event.replyToken;
if (msg.startsWith("登録")) {
replyText(replyToken, registerEvent(msg));
} else if (msg.startsWith("削除")) {
replyText(replyToken, deleteEvent(msg));
} else if (msg === "一覧") {
replyText(replyToken, listEvents());
} else if (msg === "help") {
replyText(replyToken, helpMessage());
} else {
replyText(replyToken, "help と送ると使い方がわかるよ!");
}
}
// ===== ⑤日付フォーマッタ =====
function ymd(d) {
if (!d) return "";
// 1. すでに文字列で、短い形式(MM/DD)ならそのまま返す
if (typeof d === "string" && d.includes("/") && d.length <= 5) return d;
// 2. 日付オブジェクト、または変換可能な文字列を整形する
try {
let dateObj = (d instanceof Date) ? d : new Date(d);
if (!isNaN(dateObj.getTime())) {
return Utilities.formatDate(dateObj, "Asia/Tokyo", "yyyy/MM/dd");
}
} catch(e) {}
return d;
}
// ===== ②イベント登録(変更反映後) =====
function registerEvent(text) {
try {
let parts = text.split(/[\s ]+/); // 半角・全角両方空白
if (parts.length < 3) return "✖形式が違うよ\n例)登録 推し誕 03/15 URL(なくてもOK)";
let name = parts[1];
let dateStr = parts[2];
let url = parts[3] || "";
// 日付フォーマットチェック(数値と "/" のみを許容する)
if (!dateStr.match(/^(\d{2,4}\/\d{1,2}\/\d{1,2}|\d{1,2}\/\d{1,2})$/)) {
return "✖日付の形式が違うよ\nyyyy/MM/dd か MM/dd のみ入れて下さい";
}
let yearless = (dateStr.length <= 5);
let sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
// 既存ID取得 (2行目から)
let values = sheet.getDataRange().getValues();
let usedIds = values.slice(1).map(r => Number(r[0])).filter(x => !isNaN(x));
let id = 1;
while (usedIds.includes(id)) id++;
// 必ず文字列で保存
sheet.appendRow([id, name, dateStr, url, yearless, ""]);
return `登録したよ!\nID:${id}\n${name}\n${dateStr}\n${url}`;
} catch (e) {
return "登録に失敗しました。";
}
}
// ===== ⑥削除(追加) =====
// 形式)削除 12
function deleteEvent(text) {
// 入力値の防御
if (typeof text !== 'string' || text.trim() === '') {
return "削除コマンドを正しく入力してください\n例)削除 ID";
}
let parts = text.split(/[\s ]+/); // 半角・全角両方スペース対応
if (parts.length < 2) return "削除コマンドの書式が違うよ!\n例)削除 123";
let target = Number(parts[1]);
if (isNaN(target)) return "IDが正しく指定されていないかも";
let sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
let values = sheet.getDataRange().getValues();
for (let i = 1; i < values.length; i++) {
if (values[i][0] == target) {
sheet.deleteRow(i + 1);
return `削除したよ!\nID:${target}`;
}
}
return "イベントが見つかりませんでした…";
}
// ===== ③一覧(変更反映済) =====
function listEvents() {
let sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
let values = sheet.getDataRange().getValues();
if (values.length < 2) return "まだ何も登録されていないよ!";
let msg = "📅登録イベント一覧\n";
for (let i = 1; i < values.length; i++) {
const r = values[i];
const dateDisplay = ymd(r[2]);
msg += `ID:${r[0]} ${r[1]} ${dateDisplay}\n`;
}
return msg;
}
// ===== ⑦HELP(追加) =====
function helpMessage() {
return `📌使い方
📍登録(イベント名に空白は入れないでね)
登録 イベント名 日付 [URL]
例1: 毎年通知させたいとき)
登録 推し誕 12/24
例2: 一回だけ通知させたいとき)
登録 推し誕 2025/12/24
例3: 通知にURLもつけたいとき)
登録 ライブ当落 2025/01/01 URL
📍削除
削除 ID
📍一覧
一覧
`;
}
// ===== ⑧毎日トリガー通知(追加) =====
function notifyEvents() {
let sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
let values = sheet.getDataRange().getValues();
let today = new Date();
today.setHours(0,0,0,0);
let todayStr = Utilities.formatDate(today, "Asia/Tokyo", "yyyy/MM/dd");
let thisYear = today.getFullYear(); // 実行時の「西暦」を数値で取得
for (let i = 1; i < values.length; i++) {
let [id, name, dateStr, url, yearless, notified] = values[i];
if (!dateStr) continue;
if (dateStr instanceof Date) {
dateStr = Utilities.formatDate(dateStr, "Asia/Tokyo", "yyyy/MM/dd");
} else {
dateStr = String(dateStr).trim();
}
// --- 1. 日付データのクレンジング ---
let cleanDateStr = String(dateStr).trim().replace(///g, "/");
let isYearless = String(yearless).toUpperCase() === "TRUE";
let targetDate;
// --- 2. 日付オブジェクトの作成(年またぎ対応) ---
if (isYearless) {
// スラッシュの数を確認(1個なら月日のみ、2個なら年あり)
let parts = cleanDateStr.split('/');
if (parts.length === 2) {
// "1/1" 形式の場合
targetDate = new Date(`${thisYear}/${cleanDateStr}`.replace(/\//g, "-"));
} else {
// "2025/1/1" 形式の場合(シートの自動補完でこちらになりやすい)
targetDate = new Date(cleanDateStr.replace(/\//g, "-"));
}
targetDate.setHours(0, 0, 0, 0);
// ★今日より過去日なら、無条件で「翌年」にする
if (targetDate < today) {
targetDate.setFullYear(targetDate.getFullYear() + 1);
}
} else {
// 年あり設定(FALSE)なら、入力された年をそのまま使う
targetDate = new Date(cleanDateStr.replace(/\//g, "-"));
targetDate.setHours(0, 0, 0, 0);
}
// --- 3. 差分計算 ---
if (!targetDate || isNaN(targetDate.getTime())) continue;
let diff = Math.round((targetDate - today) / (1000 * 60 * 60 * 24));
notified = (notified || "").toString();
let notifiedFlags = {};
if (notified.length > 0) {
notified.split(',').forEach(s => {
let arr = s.split(':');
if (arr.length === 2 && arr[0] === todayStr) notifiedFlags[arr[1]] = true;
});
}
let msg = null;
if (diff === 7 && !notifiedFlags[7]) msg = `✨「${name}」7日前だよ!\n楽しみだね!`;
if (diff === 1 && !notifiedFlags[1]) msg = `✨明日は「${name}」だよ!\nワクワクだね!`;
if (diff === 0 && !notifiedFlags[0]) msg = `✨今日はいよいよ「${name}」だよ!`;
// 通知を送信した場合は、必ず履歴(F列)も追加
if (msg) {
if (url) msg += `\n${url}`;
pushText(msg);
let flagStr = notified.length > 0 ? (notified + "," + `${todayStr}:${diff}`) : `${todayStr}:${diff}`;
sheet.getRange(i + 1, 6).setValue(flagStr);
}
}
}
// ===== LINE送信(返信と通知) =====
// ④reply
function replyText(token, message) {
UrlFetchApp.fetch("https://api.line.me/v2/bot/message/reply", {
"method": "post",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN
},
"payload": JSON.stringify({
"replyToken": token,
"messages": [{ "type": "text", "text": message }]
})
});
}
// ⑨push(追加)
function pushText(message) {
if (!message || message.trim() === "") return;
try {
const res = UrlFetchApp.fetch("https://api.line.me/v2/bot/message/push", {
"method": "post",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN
},
"payload": JSON.stringify({
"to": PUSH_USER_ID,
"messages": [{ "type": "text", "text": message }]
})
});
Logger.log("LINEレスポンス: " + res.getContentText());
} catch(e) {
Logger.log("LINE送信エラー:" + e);
}
}
3.3 意識したこと
📍LINE送信フォーマットのルール
Botが正しく命令を理解できるよう、本コードは以下のようなフォーマットルールを定義して実装しています。
- 各項目間に半角スペース1個を入れる
- 指定する各項目内ではスペースを入れてはいけない
- 日付は yyyy/MM/dd もしくは MM/dd
| 機能 | メッセージフォーマット | 例 |
|---|---|---|
| 登録 | 登録 [イベント名] [日付] [URL(任意)] | 登録 ライブ 2025/12/25 |
| 一覧 | 一覧 | 一覧 |
| 削除 | 削除 [ID番号] | 削除 5 |
| ヘルプ | help | help |
注意
ルールに従わないと想定している挙動にならないため注意してください。
ヘルプメッセージ機能でフォーマットの確認はいつでも行うことができます。
📍登録データに対する日付フォーマットチェック
このBotは、日付の書き方によって 「通知のスタイル」 を自動で判別します。
-
毎年祝いたい場合(誕生日など)
- MM/dd 形式で入力:例 登録 推し誕 10/25
- 挙動:毎年10/25に通知
-
一回きりの予定の場合(ライブなど)
- yyyy/MM/dd 形式で入力:例 登録 ライブ 2025/05/10
- 挙動:2025年のその日が終われば、翌年以降は通知なし
しかし、日付に半角や全角が混在しているなどユーザーが想定外の形式で日付を入力した際にプログラム側でエラーが起きてしまう可能性が高いです。実際、年なしと年ありの書き分けをした際に日付が日付型として扱われる場合と文字列型として扱われる場合があり通知のスタイルの自動判別が初めはできていませんでした。
そのため本コードでは 「入力・保存・表示」は一貫して文字列として扱う といった仕様に修正したことで、表記の揺れや意図しない自動変換を防いでいます。また、 「当日まであと何日?」という計算を行う瞬間だけ日付オブジェクトに変換する という適材適所な使い分けをしています。
4. トリガー設定
トリガー設定とは、GASのプログラムを定刻に動作させるための「目覚まし時計」のような設定のことです。これを行うことで、自動でリマインド通知が届くようになります。
まず、GASの左側にある時計マークのトリガーをクリックします。
次にトリガーを追加から、以下のようにトリガーの設定を行います。
トリガーを実行するタイミングを、週ベースか、日付ベースか、時間ベースかなど、自分の好きなように設定することができます。
今回筆者は毎日午前8時~9時の間に実行されるよう設定しました。
保存を押すと、その瞬間からトリガーが適用されます。
トリガー設定ではエラー率なども確認できるため、メンテナンスに活用することもできます。
5. デプロイ・Webhook設定
最後にデプロイを行います。
GASの右上にあるデプロイ→新しいデプロイをクリックします。
デプロイを完了するとURLが発行されます。
URLはデプロイ→デプロイを管理より、いつでも確認することができます。
このURLをコピーして、DevelopersのWebhook設定のURL欄に貼り付けます。
この際Webhook利用はONにしてください。
Webhookとは、特定のイベントが発生した際に、指定したURL(Botサーバー)へLINEプラットフォームからHTTP POSTリクエストを送信し、自動的に通知を行う仕組みです。
(参考:Webhook | LINE API UseCase)
また同様に、LINE Official Account Managerの設定→Messaging APIのWebhook URL欄にも貼り付けます。
6. テスト
以上で推し活リマインドBotの設定が終了したので、実装したコードが想定している動作をするか確認するための以下のテストを行いました。
6.1 登録・削除・ヘルプ・一覧表示ができるか
- 登録機能のテスト
| チェック項目 | 確認観点(例) |
|---|---|
| 日付(年あり) | 「登録 ライブ申込日 2025/12/22 URL」でID:1が生成され、URLも保存されているか |
| 日付(年なし) | 「登録 推し誕生日 12/29」で、年を除いた形式でも登録できているか |
| バリデーション | 日付のフォーマットが異なった際に「✖ 日付の形式が違うよ」とエラーを返し、不正な登録を防げているか |
| 再登録時の挙動 | 削除後に新しい予定(結成日)を登録した際、IDが適切に割り振られているか |
- 削除機能のテスト
| チェック項目 | チェックポイント |
|---|---|
| 指定削除 | 「削除 3」と送った後、ID:3のデータが即座に一覧から消えているか |
- 一覧表示機能のテスト
| チェック項目 | 確認観点(例) |
|---|---|
| フォーマット整形 | スプレッドシートでは「12/29」と保存されているデータが、一覧表示では「2025/12/29」のようにymd関数によって年が補完・整形されているか |
| 未登録時の挙動 | 最初に「一覧」と送った際、「まだ何も登録されていないよ!」と正しく返信されている |
- ヘルプ機能のテスト
| チェック項目 | 確認観点(例) |
|---|---|
| キーワード反応 | 「help」と送信して、あらかじめ用意したガイド文が返ってくるか |
- スプレッドシートの最終状態チェック
| チェック項目 | 確認観点(例) |
|---|---|
| IDの管理 | 1から順番にIDが重複せずに振られているか |
| 年なしフラグ | E列に「年なし」かどうかの判定が正しいか |
| ymd(d) | 日付の表示形式を整える |
テスト項目に沿ってBotに対してトークでメッセージを送信したところ、以下のようなLINE Bot・スプレッドシートともに想定していた結果を得ることができました。
これにより、登録・削除・ヘルプ・一覧表示が正常に動作することを確認できました。
6.2 自動通知(notifyEvents関数)が正しく動くか
- 通知タイミング(7日前/1日前/当日)のテスト
| チェック項目 | 確認観点(例) |
|---|---|
| キーワード反応 | 「help」と送信して、あらかじめ用意したガイド文が返ってくるか |
| 当日通知 | 当日イベントに関して「今日はいよいよ...」と届くか |
| 1日前通知 | 明日のイベントに関して「明日は...」と届くか |
| 7日前通知 | 1週間後のイベントに関して「7日前だよ!」と届くか |
| 対象外 | 1週間後/1日後/当日以外のイベントに関して通知が来ないか |
テストデータに12/25のイベントを追加し2025/12/22を迎えてみたところ、以下のように想定していたトリガーの時間内に通知が届き、LINE Bot・スプレッドシートともに想定していた結果を得ることができました。
- 年なしイベントの繰り越しのテスト
| チェック項目 | 確認観点(例) |
|---|---|
| 年またぎ補正 | 12/25時点で「1/1」の予定を翌年のものとして計算し、「7日前」通知が届くか |
| 履歴の独立性 | 過去(2025/1/1)に通知済み履歴があっても、新年度分として通知が送られるか |
「年なし」設定の予定が、年を跨いで正しく通知されるかを検証しました。 スプレッドシート内部では日付に「現在の年」が自動補完されるため、12月末に実行した際、翌年1月の予定が「過去(2025年1月)」と判定されるリスクがあります。 これを回避する 「年またぎ補正ロジック」 が正しく機能し、1/1の予定を「2026年」として認識できるかをテストしました。
テストデータに1/1のイベントを追加する、かつnotifyEvents関数に以下の変更を加えて手動で関数を実行して検証しました。
function notifyEvents() {
let sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
let values = sheet.getDataRange().getValues();
// --- テスト用:2025年の12月25日だと仮定する ---
let today = new Date("2025/12/25");
today.setHours(0,0,0,0);
// ...以下、元のコード------------------------------
// 内部ロジックのポイント:
// ターゲットが1/1(2025/1/1)の場合、today(2025/12/25)より過去と判定
// 自動的に +1年 され、2026/1/1 として計算(差分7日)されることを確認
すると、以下のように通知が届き、LINE Bot・スプレッドシートともに想定していた結果を得ることができました。
これにより、設定したタイミング(7日前/1日前/当日)による自動通知と、一度登録すれば毎年決まったタイミングで通知が来る 「永久リマインド」の動作 を確認できました。
7. Geminiとの連携
結論から言うと、執筆時点ではGeminiをVSCodeのような対話型IDEとしてGAS上で利用することはできませんでした。
現時点では、Google Apps Scriptエディタを拡張するための公式なAPIやプラグイン機構は公開されていません。しかし、「Apps Scriptエディタに拡張機能を追加するChrome拡張」は存在します1が、非公式でありGoogleのサポート対象外のため本記事では利用しません。
一方で、設計や実装の壁打ち相手としては非常に有効でした。
7.1 期待していたこと
筆者が当初期待していたのは、VSCode + Copilotのような使い方です。
たとえば、GASエディタ上で
- 「この関数を書いて」など指示すると、即座にプログラムへ反映される
- エラー発生時にエラーログをもとに即修正案を提案してくれ、ワンクリックで修正を適用できる
など
7.2 実際にはできなかったこと
章の初めにお伝えしたとおり、VSCodeのような対話型IDE連携は実現できず、以下のような「エディタにAIが常駐する」といった使い方はできませんでした。
- GASエディタ内でGeminiとリアルタイムに対話する
- コード補完・自動修正を即時反映する
- ファイル単位でのリファクタリング
- デバッグ中に対話で修正する
7.3 なぜGemini × GASの対話型IDE連携が難しいのか
GAS上でGeminiをVSCode + Copilotのような対話型IDEとして利用できないのは、GAS自体の仕組みや設計に起因する部分が大きいと考えられます。
ブラウザベースIDEであることによる制約
GASのエディタは、VSCodeのようなローカルIDEではなく、ブラウザ上で動作するWebベースのエディタです。
(参考:Google Apps Script 公式ページ)
そのため、以下のような IDE と密接に連携する機能 は提供されていません。
- 編集中のコードをリアルタイムに監視する
- カーソル位置に応じてコード補完を差し込む
- エラー発生時に、その場でコードを修正・反映する
VSCodeなどのローカルIDEでは、拡張機能を通してこれらの機能を実現できますが、GASはブラウザ上のエディタで完結する比較的シンプルな設計となっています。
そのため、拡張ポイントやプラグインAPIなどが公式には用意されておらず、現時点ではVSCodeのような対話型IDEを実現することは難しいと考えられます。
7.4 代替として有効だった使い方
そこで筆者が考えるGeminiの有効だった使い方は以下のとおりです。
1. Google Workspace拡張機能の活用
今回の開発で特に役立ったのが、GeminiにGoogleドライブ上のスプレッドシートを直接読み込ませるGoogle Workspace拡張機能です。
ブラウザのIDE単体では不可能な、「スプレッドシートの今の状態」を踏まえたやり取りが可能になりました。
Google Workspace拡張機能としてドライブ内のデータにアクセスするには、Gmailの設定で以下二つの項目を両方ONに設定する必要があります。
拡張機能によりドライブ内のデータへアクセスできるようになったことで、
-
スプレッドシートの構造に合わせた「専用コード」の生成
-
テストデータの自動生成
→ GAS特有のコードの書き方やテストデータをすぐ確認でき、効率的に開発・検証することができました。
読み取り可能なデータには制限がある
GeminiのGoogle Workspace拡張機能が読み取りをサポートしているファイル形式には、現在GASは含まれていません。そのため、「ドライブにあるGASファイルを読み込んでデバッグして」という指示には応えられませんでした。
2. 設計の壁打ち
-
Webhook構成の相談
-
スプレッドシートの設計
-
機能分割(登録 / 削除 / 一覧)
→ 実装前の迷いを大きく減らすことができました。
3. エラー調査の補助
-
実行ログをそのまま貼り付け、原因候補と修正案の提示
→ 検索するよりも早く原因を特定でき、初心者でも理解しやすかったです。
AI利用時の注意
本記事では生成AI(Gemini)を開発の補助として活用していますが、 AIが生成したコードや文章は常に正しいとは限りません。
最終的な仕様判断や実装内容の正誤については、必ず人間の目で確認・検証したうえで利用してください。
おわりに
LINE BotとGASを連携する開発は想像以上に手軽で、メッセージ応答から通知、データ管理までをサーバーレスで完結できる点が印象的でした。「まず動くものを作る」という意味では、非常に相性の良い組み合わせだと感じました。
しかし、GeminiとブラウザのIDE上で対話しながらともにGASを実装する体験は叶いませんでした。(執筆当時)
個人的には、Geminiは調査や方針整理の場面で非常に頼もしく、開発の前段階では大いに助けられました。いつかブラウザIDEでも、思考と実装がシームレスにつながる対話型の開発体験になることを願っています。
本記事が、同様の取り組みを行う際の一助となれば幸いです。
参考文献
本記事を執筆するにあたり、既存の資料や記事を参考にさせていただきました。
本記事で補いきれていない点については、ぜひ下記の記事もあわせてご参照いただけますと幸いです。




















