はじめに
暇なので作りました。
技術スタック
GAS + HTML(Bootstrap) + Google Calendar API。
こだわりポイント
コード部分はすべてGeminiに作ってもらいました。
- スプレッドシートで「稼働時間」「バッファ」「最大連続枠」を管理
- JSでの複数スロット選択と、バックエンドでの1つのイベントへの集約
ハマったところ
Calendar.Events.patch でのGoogle Meet発行とAPI有効化の罠
罠:createEvent だけではMeetURLが作れない
GASの標準機能である CalendarApp.createEvent には、2026年現在も「Google Meetを発行する」というオプションがありません。
そのため、一度予定を作成した後に Google Calendar API (Advanced Service) を使って「会議データ」を後付けする必要があります。
APIの有効化が「2箇所」必要
ここが最大のハマりポイントでした。
-
GASエディタ上の設定: 左メニューの「サービス +」から「Google Calendar API」を追加
-
Google Cloud Console (旧APIコンソール): 以前はここも必須でしたが、現在はGASエディタ側だけで済むことが多いです。ただし、他人に配布して「コピー」させた場合、コピーした側のエディタで 再度「サービス追加」をしないと400エラーで落ちます
-
createEvent だけではMeetのURLが作れない
GASの標準機能である CalendarApp.createEvent には、2026年現在も「Google Meetを発行する」というオプションがありません。
そのため、一度予定を作成した後にGoogle Calendar API (Advanced Service)を使って「会議データ」を後付けする必要があります。 -
event.getId()の末尾問題
event.getId()で取得できるIDには@google.comが付いていますが、Calendar.Events.patchに渡すIDにはこれを含めてはいけません。
.split('@')[0]で削らないと、これもまた400 Bad Requestを食らうことになります。
実装コード
Meetを確実に発行するためのパッチ処理がこちらです。
// 1. まず普通に予定を作る
const event = calendar.createEvent("予約完了", startTime, endTime, options);
// 2. Meet発行用のリソースを定義
const resource = {
conferenceData: {
createRequest: {
requestId: "req_" + Date.now(),
conferenceSolutionKey: { type: "hangoutsMeet" }
}
}
};
// 3. APIを叩いてMeetを紐付ける
// ※ここで Calendar API (Advanced Service) を追加していないと ReferenceError になります
Calendar.Events.patch(
resource,
calendar.getId(),
event.getId().split('@')[0], // IDの末尾の「@google.com」を削るのがコツ
{ conferenceDataVersion: 1 }
);
new Date() の型変換とミリ秒計算のデバッグ
予約システムで「30分枠を2つ選んだら1時間にする」といった計算をする際、JavaScriptのDate型はそのまま足し算ができません。
ここでハマらないためのポイントをまとめました。
-
Date同士の計算は「数値」に直さないとズレる
GAS(JavaScript)では、Dateオブジェクトに数値を足しても時間は増えません。一度1970/1/1からの経過ミリ秒に変換するのが最も安全です。
// 30分(slotDuration)を足したい場合
const durationMs = config.slotDuration * 60 * 1000; // 分をミリ秒に変換
const startTime = new Date("2026/02/26 10:00");
// ❌ これではダメ(文字列結合や予期せぬ挙動の原因)
// const endTime = startTime + durationMs;
// ✅ これが正解(getTime()で数値にしてから足し、Dateに戻す)
const endTime = new Date(startTime.getTime() + durationMs);
- GASの Utilities.formatDate とタイムゾーン
計算した結果をスプレッドシートやフロントエンドに返す際、単に.toString()するとサーバーのタイムゾーンに引っ張られることがあります。
// 日本時間(JST)で確実にフォーマットする
const formattedTime = Utilities.formatDate(new Date(), "JST", "HH:mm");
- スプレッドシートからの「時刻」読み込み
スプレッドシートのセルからgetValues()で時刻を取得すると、GAS側では自動的にDateオブジェクトとして認識されます。
しかし、たまに「文字列」として入ってくることもあるため、以下のようなガード句を挟みます。
const formatTime = (val) => {
if (val instanceof Date) {
// Date型ならフォーマット
return Utilities.formatDate(val, "JST", "HH:mm");
}
// 文字列ならそのまま、または正規表現でチェック
return String(val);
};
- ループ処理での「数値比較」
「稼働開始時間から終了時間まで30分刻みでボタンを作る」といったループでは、Dateオブジェクトを直接比較するのではなく、数値(ミリ秒)で比較すると無限ループを防げます。
let currentPos = startLimit.getTime();
const endPos = endLimit.getTime();
while (currentPos + durationMs <= endPos) {
// 処理...
currentPos += durationMs; // ミリ秒単位で加算していく
}