ドローン練習場の予約システムをGoogleフォーム+スプレッドシート+カレンダーで実装した方法

この記事では、株式会社チックがどのようにドローン練習場の予約システムを構築したかをご紹介します。

具体的には、Googleフォームで「練習場の予約」を受け付け、スプレッドシートとApps Scriptを介してGoogleカレンダーに反映する仕組みです。

現在の仕様では、予約のみを扱い、キャンセル機能は実装していませんが、予約時にイベントIDを発行しており、今後キャンセル機能を追加する際に活用する想定です。

システム概要

  • Googleフォームでドローン練習場の予約情報を受け付け
  • スプレッドシートに回答が蓄積される
  • Apps Script (onFormSubmit) でカレンダーに予定を自動登録
  • 重複チェックや「全日」対応
  • エラー(重複・日付不正など)の場合は予約者へメール通知
  • 予約が完了すると「イベントID」を発行し、予約確定メールを送信

キャンセル機能は現状ありませんが、将来的にイベントIDを活用したキャンセル機能を拡張予定です。


準備するもの

  1. Googleフォーム
    – 質問例: メールアドレス、団体名、氏名、電話番号、予約希望日、利用希望時間(「全日」や「09:00-10:00」など)、備考
    – 回答をスプレッドシート「予約管理」に記録
  2. スプレッドシート
    – 例: 「予約管理」という名のシートを用意
    – 列構成: A=タイムスタンプ, B=メール, C=団体名, D=氏名, E=電話, F=予約日, G=利用希望時間, H=備考, I=ステータス, J=イベントID
  3. Googleカレンダー
    – ドローン練習場のスケジュールを可視化するカレンダー
    – 「カレンダーID」を取得し、Apps Script側で設定 (権限は編集可能に)
  4. Apps Script
    – スプレッドシートと同じファイル(コンテナバウンド)で管理 (推奨)
    – 「フォーム送信時」のトリガーを設定 → onFormSubmit関数でカレンダー登録

実装コード (キャンセル機能なし)

以下が、株式会社チックが構築した「予約のみ対応」のApps Scriptコードです。
フォームから送信されるデータを元に、カレンダーに予定を登録します。
カレンダー取得エラーや重複などが起こった場合、ステータス列(I列)に内容を記録し、予約者へ「予約不可メール」を送信します。
成功時は「予約確定」と記録し、イベントIDをJ列に保存し、予約完了メールを送ります。

// ----------------------------
// Googleフォーム送信時 (予約のみ対応)
// ----------------------------
function onFormSubmit(e) {
  const responses = e.values;
  // A=timestamp(0), B=email(1), C=orgName(2), D=personName(3),
  // E=phone(4), F=reservationDate(5), G=timeRaw(6), H=note(7)

  const email           = responses[1]; // B列
  const orgName         = responses[2]; // C列
  const personName      = responses[3]; // D列
  const phone           = responses[4]; // E列
  const reservationDate = responses[5]; // F列
  const timeRaw         = responses[6]; // G列
  const note            = responses[7]; // H列

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("予約管理");
  const lastRow = sheet.getLastRow();

  // カレンダーID を実際のものに変更
  const calendarId = "xxxxx@group.calendar.google.com";
  const calendar   = CalendarApp.getCalendarById(calendarId);

  Logger.log("=== onFormSubmit: 予約 ===");
  Logger.log("calendar: " + calendar); // デバッグ用

  if (!calendar) {
    setStatusAndReject(sheet, lastRow, email, "カレンダー取得エラー");
    return;
  }

  // 予約希望日のDate
  const dateObj = new Date(reservationDate);
  if (isNaN(dateObj.getTime())) {
    setStatusAndReject(sheet, lastRow, email, "日付エラー");
    return;
  }

  // 利用希望時間をカンマ区切りで解析
  const halfWidth = toHalfWidth(timeRaw || "");
  const timeRangeList = halfWidth.split(",").map(s => s.trim()).filter(Boolean);
  if (timeRangeList.length === 0) {
    setStatusAndReject(sheet, lastRow, email, "利用希望時間未入力");
    return;
  }

  let createdIds = [];

  for (let i = 0; i < timeRangeList.length; i++) { const timeRange = timeRangeList[i]; let startDateTime, endDateTime; // "全日" は 0:00~翌0:00 if (timeRange === "全日") { startDateTime = new Date(dateObj); startDateTime.setHours(0, 0, 0, 0); endDateTime = new Date(dateObj); endDateTime.setDate(endDateTime.getDate() + 1); endDateTime.setHours(0, 0, 0, 0); } else { // 09:00-10:00 等 const parsed = parseDateTimeRange(dateObj, timeRange); if (!parsed.startDateTime || !parsed.endDateTime) { setStatusAndReject(sheet, lastRow, email, "時間形式エラー"); return; } startDateTime = parsed.startDateTime; endDateTime = parsed.endDateTime; } // 重複チェック const events = calendar.getEvents(startDateTime, endDateTime); if (events.length > 0) {
      setStatusAndReject(sheet, lastRow, email, "重複");
      return;
    }

    // カレンダーに登録
    const event = calendar.createEvent("予約あり", startDateTime, endDateTime, {
      description:
        "団体名: " + orgName +
        "\n氏名: " + personName +
        "\n電話: " + phone +
        "\n利用時間: " + timeRange +
        "\n備考:\n" + (note || "")
    });
    createdIds.push(event.getId());
  }

  // 成功 → I列=「予約確定」, J列=イベントID
  sheet.getRange(lastRow, 9).setValue("予約確定");
  sheet.getRange(lastRow,10).setValue(createdIds.join(","));

  // 完了メールを送信
  sendCompleteMail(email, orgName, personName, phone, dateObj, timeRangeList, note, createdIds);
}

// 失敗時: ステータス+不可メール
function setStatusAndReject(sheet, lastRow, email, errMsg) {
  sheet.getRange(lastRow, 9).setValue(errMsg);
  sendRejectMail(email, errMsg);
}

// 不可メール
function sendRejectMail(email, reason) {
  if (!email) return;
  const subject = "【ドローン練習場】予約不可のお知らせ";
  const body =
    "下記の理由により予約を受け付けられませんでした。\n\n" +
    "理由:" + reason + "\n\n" +
    "お手数ですが、内容をご確認のうえ再度お試しください。";
  MailApp.sendEmail(email, subject, body);
}

// 予約完了メール
function sendCompleteMail(email, orgName, personName, phone, dateObj, timeRangeList, note, eventIds) {
  if (!email) return;
  const formatted = Utilities.formatDate(dateObj, "Asia/Tokyo", "yyyy/MM/dd");
  const timesStr = timeRangeList.join("\n");
  const subject = "【ドローン練習場】予約完了のお知らせ";

  const body =
    personName + " 様(団体名:" + orgName + ")\n\n" +
    "以下の内容で予約を受け付けました。\n" +
    "予約日:" + formatted + "\n" +
    "利用時間:\n" + timesStr + "\n\n" +
    "電話番号:" + phone + "\n" +
    (note ? "備考:\n" + note + "\n\n" : "\n") +
    "【イベントID】\n" + eventIds.join(",") + "\n\n" +
    "当日お待ちしております。";

  MailApp.sendEmail(email, subject, body);
}

// "09:00-10:00" 等 → 開始/終了解析
function parseDateTimeRange(dateObj, timeRange) {
  let delimiter;
  if (timeRange.includes("~")) delimiter = "~";
  else if (timeRange.includes("-")) delimiter = "-";
  else return { startDateTime:null, endDateTime:null };

  const [startStr, endStr] = timeRange.split(delimiter).map(s=>s.trim());
  if (!startStr || !endStr) {
    return { startDateTime:null, endDateTime:null };
  }

  const [startH, startM] = startStr.split(":");
  const [endH, endM]     = endStr.split(":");
  if (!startH || !startM || !endH || !endM) {
    return { startDateTime:null, endDateTime:null };
  }

  const startDateTime = new Date(dateObj);
  startDateTime.setHours(parseInt(startH,10), parseInt(startM,10), 0,0);
  const endDateTime = new Date(dateObj);
  endDateTime.setHours(parseInt(endH,10), parseInt(endM,10), 0,0);

  return { startDateTime, endDateTime };
}

// 全角→半角
function toHalfWidth(str) {
  return str.replace(/[!-~]/g, function(s) {
    return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
  });
}

カレンダーに反映されない場合の確認項目

カレンダーIDの確認

  • 「設定と共有」画面の「カレンダーID」を正しくコピペしているか
  • 前後に余計な空白や改行が入っていないか

権限

  • スクリプト実行アカウントがそのカレンダーを「予定の変更権限以上」で編集できるか
  • 「calendar: null」とログに出るなら、ID不正 or 権限不足が疑われる

トリガー設定

  • Apps Scriptで「onFormSubmit」×「フォーム送信時」のトリガーを設定しているか
  • 初回保存時にカレンダーアクセス許可ダイアログが出るので、承認が必要

列ズレ

  • 本コードではB列=メール、C=団体名、D=氏名、E=電話、F=予約日、G=利用時間、H=備考、I=ステータス、J=イベントID
  • 1つ質問を追加/削除すると列がズレてしまうので注意

シートI列(ステータス)

  • “予約確定” → カレンダー登録成功
  • “重複” / “日付エラー” / “カレンダー取得エラー” など → その原因を対処

まとめ

株式会社チックが今回ご紹介したコードは、ドローン練習場の予約をGoogleフォームで受付し、スプレッドシートとGoogleカレンダーへ自動連携する仕組みを実現します。
「予約のみ」の実装ですが、重複チェック、全日対応、予約完了メールなど基本機能は一通り備えています。
いずれキャンセル機能を追加したい際にも、イベントIDを活用できますのでご安心ください。
ぜひこのコードを参考に、ドローン練習場の予約業務を効率化してみてください。

関連記事

Google Serch ConsoleをWordPressに登録・設定する方法を解説!

【初心者必見!】Google Analytics 4をWordPressに導入する方法

PAGE TOP