3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

📦 Googleフォーム × LINE で「レンタルWi-Fiの返却忘れ防止システム」を自作した話

3
Last updated at Posted at 2025-07-18

こんにちは。tampopo256です。

はじめに

イベントでモバイルWi-Fiなどの機材をレンタル会社から借りて運用する場面が多いのですが、

  • ✅ 誰が何を借りたか分からない
  • ✅ 返却日を忘れてしまう
  • ✅ 返却漏れが発生し延滞料金の発生

…そんな地味だけど大きなトラブルを防ぐために、
Googleフォーム × Googleスプレッドシート × LINE公式アカウント を使って、
誰でも確実に返却できるしくみを構築しました。

🎯 このシステムでできること

  • 📄 借りたWi-Fiルーターに「返却期限入りの紙+QRコード」を貼る
  • 📱 返却時にQRコードを読み取ってGoogleフォームから申請
  • 🗂 スプレッドシートで返却状況を自動更新
  • 🔔 LINEグループに「返却完了」や「今日返却のものがあります」などの通知が届く
  • 💬 LINEで @登録@解除 と打てば、通知先グループの管理もできる

📷 QRコード付き紙のサンプル(貼付用)

名称未設定のデザイン (2).png

📗 スプレッドシート構成(貸出台帳)

貸出ID 貸出名 借用者名 借用日 返却期限 ステータス 返却者名 最終更新日時
MW-001 モバイルWi-Fi A 田中太郎 7/15 7/17 17:00 未返却

📄 Googleフォームの質問内容

質問 形式 備考
貸出ID テキスト 貼り付けた紙に記載のIDを入力
返却者名 テキスト 必須にする
コメント 段落 任意(破損・欠品等あれば記入)

🧠 実装したこと(GAS編)

✅ フォーム送信時にスプレッドシート更新+LINE通知

function onFormSubmit(e) {
  const responses = e.namedValues;
  const lendId = responses["貸出ID"]?.[0]?.trim();
  const returner = responses["返却者名"]?.[0]?.trim() || "(未記入)";
  if (!lendId) return;

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("貸出台帳");
  const data = sheet.getDataRange().getValues();

  for (let i = 1; i < data.length; i++) {
    const row = data[i];
    if (row[0] === lendId && row[5] !== "返却済") {
      const itemName = row[1];
      sheet.getRange(i + 1, 6).setValue("返却済");
      sheet.getRange(i + 1, 7).setValue(returner);
      sheet.getRange(i + 1, 8).setValue(new Date());
      sendGroupNotification(`📦 ${itemName}(ID: ${lendId})が ${returner} により返却されました。`);
      break;
    }
  }
}

✅ その日の返却期限一覧を朝に通知

function notifyTodayDueReturns() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("貸出台帳");
  const data = sheet.getDataRange().getValues();
  const today = new Date();
  const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate());
  const endOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59);

  const dueList = [];

  for (let i = 1; i < data.length; i++) {
    const lendId = data[i][0];
    const itemName = data[i][1];
    const dueDate = new Date(data[i][4]);
    const status = data[i][5];

    if (status !== "返却済" && dueDate >= startOfDay && dueDate <= endOfDay) {
      dueList.push(`🔔 ${itemName}(ID: ${lendId}) → 本日返却期限`);
    }
  }

  if (dueList.length > 0) {
    const message = `⏰ 本日返却期限の貸出があります:\n\n${dueList.join("\n")}`;
    sendGroupNotification(message);
  }
}

✅ LINEグループ通知(Push API)

function sendGroupNotification(message) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("通知グループ");
  const data = sheet.getDataRange().getValues();
  const accessToken = "YOUR_CHANNEL_ACCESS_TOKEN";

  for (let i = 1; i < data.length; i++) {
    const groupId = data[i][1];
    const payload = {
      to: groupId,
      messages: [{ type: "text", text: message }],
    };
    const options = {
      method: "post",
      contentType: "application/json",
      headers: { Authorization: "Bearer " + accessToken },
      payload: JSON.stringify(payload),
    };
    UrlFetchApp.fetch("https://api.line.me/v2/bot/message/push", options);
  }
}

✅ LINEで通知グループの登録/解除

function doPost(e) {
  const json = JSON.parse(e.postData.contents);
  const event = json.events?.[0];
  const groupId = event?.source?.groupId;
  const replyToken = event?.replyToken;
  const text = event?.message?.text?.trim();

  if (!groupId || !replyToken || !text) return;

  if (text === "@登録") handleGroupRegistration(groupId, replyToken);
  else if (text === "@解除") handleGroupUnregister(groupId, replyToken);
}

function handleGroupRegistration(groupId, replyToken) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("通知グループ");
  const data = sheet.getDataRange().getValues();
  const alreadyExists = data.some(row => row[1] === groupId);

  if (alreadyExists) {
    replyToLine(replyToken, "⚠️ このグループはすでに登録されています。");
    return;
  }

  sheet.appendRow(["グループ", groupId, new Date()]);
  replyToLine(replyToken, "✅ このグループを通知先として登録しました!");
}

function handleGroupUnregister(groupId, replyToken) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("通知グループ");
  const data = sheet.getDataRange().getValues();
  for (let i = 1; i < data.length; i++) {
    if (data[i][1] === groupId) {
      sheet.deleteRow(i + 1);
      replyToLine(replyToken, "🗑 このグループを通知先から解除しました。");
      return;
    }
  }
  replyToLine(replyToken, "⚠️ このグループは登録されていません。");
}

function replyToLine(replyToken, message) {
  const token = "YOUR_CHANNEL_ACCESS_TOKEN";
  const payload = {
    replyToken: replyToken,
    messages: [{ type: "text", text: message }],
  };
  const options = {
    method: "post",
    contentType: "application/json",
    headers: { Authorization: "Bearer " + token },
    payload: JSON.stringify(payload),
  };
  UrlFetchApp.fetch("https://api.line.me/v2/bot/message/reply", options);
}

📌 よかった点

  • 紙とデジタルのハイブリッドで「現場でも運用しやすい」
  • 返却フォームはQRで一発アクセス、紙は剥がせるようにして返却完了の証拠に
  • 返却者・返却日時をログとして残せる

🤔 改善したいこと

  • 期限超過時の通知(⚠️マーク付きなど)
  • フォームからの写真添付(破損報告など)
  • 管理画面(Google Apps Script UIなど)

おわりに

QRコードを入力済みのフォームが入れてあるのですが、URLが長くなりすぎてQRコードが細かくなりすぎるという問題点を解消するために、ちょっと特殊な短縮URLシステムを別途開発しています。

その時の記事も載せておくので、よければ見てみてください!!

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?