LINE × Google Sheets で作る受付順番管理システム作った
この記事でわかること
- LINE LIFF + Next.js + Google Sheets でサーバーレスな受付システムを構築する方法
- 段階通知(あと3人→2人→1人)を自動化する仕組み
- GAS(Google Apps Script)を Webhook トリガーとして活用するパターン
- 実際に躓いたポイントと解決策
このシステムで何ができるか
病院や店舗で使える受付順番管理システムを作りました。
主な機能
- LINE から受付登録
LIFFアプリで名前を入力 → 自動で番号発行 - 「順番確認」で待ち人数を返答
トークにメッセージ送信 → 即座に返信 - 段階的な自動通知
スタッフがスプレッドシートで「完了」に変更
→ 次の人に「あと3人です」→「あと2人です」→「まもなくです」と自動通知
ポイント: サーバー管理不要、月額ほぼゼロ円で運用可能
なぜこのシステムを作ったのか
きっかけは自分の体験
ある日、病院の待合室で2時間待たされたときのことです。
- 狭い待合室でずっと座っている苦痛
- いつ呼ばれるかわからない不安
- 外に出たいけど、順番を逃すかもしれない…
「同じ空間に2時間はキツすぎる。他の人も同じ気持ちなんじゃないか?」
そう思ったのが、このシステムを作るきっかけでした。
解決したかった課題
患者側:
- 待合室で2時間も密になるストレス
- 順番がわからず、外出もできない
- 「あと何人?」と何度も聞きづらい
スタッフ側:
- 患者からの問い合わせ対応に追われる
- 手動で電話やLINEで呼び出し → 手間がかかる
このシステムの可能性
病院だけじゃない。
飲食店の行列、役所の窓口…
「順番待ち」がある場所なら、どこでも応用できるはず。
待つ側も、対応する側も、みんなが楽になる。そんなシステムを目指しました。
目指したこと
- 患者が自分で状況を確認できる
- 待ち時間を有効活用(カフェ、車内、散歩など)
- スタッフの手動作業をゼロに
- 初期コスト・運用コストを最小化
技術選定の理由
システム構成
患者(LINE) ← → Vercel (Next.js) ← → Google Sheets
↕
GAS (トリガー)
| 技術スタック | 理由 |
|---|---|
| LINE LIFF | ユーザーは公式LINEから利用。アプリ開発不要 |
| Next.js (Vercel) | サーバーレス、デプロイが簡単、無料枠が大きい |
| Google Sheets | スタッフが使い慣れている。専用管理画面を作る必要なし |
| GAS | スプレッドシート編集をトリガーに Webhook 発火 |
| TypeScript | 型安全性で実装ミスを防ぐ |
なぜ RDB を使わなかったか
- 受付データは日次で完結(履歴管理は不要)
- スタッフがシートを直接編集したい
- Supabase や Firestore を使うとスタッフ用の管理画面が必要になる
→ Google Sheets で十分
実装のポイント
1. LIFF アプリで受付登録
app/page.tsx でフォームを実装。
病院で「〇〇さん〜」と受付で名前が呼ばれていると思い。
患者自身が自分の名前をフォームに入力することでLINEIDと名前を紐付けするようにしました。
// LIFF初期化
const liffModule = await import("@line/liff");
const liff = liffModule.default;
await liff.init({ liffId: LIFF_ID });
// ユーザー情報を取得
const profile = await liff.getProfile();
const lineId = profile.userId;
const name = `${lastName}${firstName}`;
// Vercel API Route に POST
await fetch("/api/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ lineId, name }),
});
ポイント:
- LIFF は LINE トーク内で開くため、ユーザーはログイン不要
profile.userIdで LINE ユーザーを一意に識別
2. Google Sheets をデータベースとして使う
lib/sheets.ts で Google Sheets API をラップ。
import { google } from 'googleapis';
const auth = new google.auth.GoogleAuth({
credentials: JSON.parse(process.env.GOOGLE_CREDENTIALS!),
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
const sheets = google.sheets({ version: 'v4', auth });
// 今日のシートにデータを追加
export async function appendRow(
spreadsheetId: string,
sheetName: string,
row: (string | number)[]
) {
await sheets.spreadsheets.values.append({
spreadsheetId,
range: `${sheetName}!A:G`,
valueInputOption: 'USER_ENTERED',
requestBody: { values: [row] },
});
}
工夫した点:
- 日付ごとにシートを分けて管理(例:
2026-02-06) - シートが存在しない場合は自動作成
3. 段階通知の自動化
スタッフがステータスを「完了」に変更 → 次の待機者に通知
3-1. GAS でスプレッドシート編集を検知
// onEdit トリガー
function onEdit(e) {
if (!e || !e.range) return;
const sheet = e.range.getSheet();
const col = e.range.getColumn();
const row = e.range.getRow();
// D列(ステータス)が変更された場合
if (col === 4 && row > 1) {
const status = sheet.getRange(row, col).getValue();
if (status === '完了') {
// Vercel の Webhook を呼ぶ
callSheetEditedWebhook(sheet.getName());
}
}
}
ポイント:
- GAS の
onEditトリガーはリアルタイムでスプレッドシート編集を検知 - Webhook を叩いて Vercel 側の処理に移譲
3-2. Vercel で段階通知ロジックを実行
const NOTIFY_THRESHOLDS = [3, 2, 1, 0];
const THRESHOLD_MESSAGES: Record<number, string> = {
3: 'あなたの前にはあと3人です。少しお待ちください。',
2: 'あなたの前にはあと2人です。準備をお願いいたします。',
1: '次の順番が近づいています。あなたの前にはあと1人です。',
0: 'まもなくご案内です。受付付近でお待ちください。',
};
export async function notifyNearTurn() {
const values = await getTodaySheetData(spreadsheetId, today);
const lastDoneNumber = getLastDoneNumber(values); // 最後に完了した番号
for (const row of values) {
const remaining = row.number - lastDoneNumber - 1; // 前に何人いるか
// 通知段階を判定
for (let i = 0; i < NOTIFY_THRESHOLDS.length; i++) {
if (remaining <= NOTIFY_THRESHOLDS[i] && row.notifyStage < i) {
await pushToUser(row.lineId, THRESHOLD_MESSAGES[i]);
await updateNotifyStage(row.number, i);
break;
}
}
}
}
工夫した点:
notifyStageでどこまで通知したかを記録- 二重通知を防ぐ仕組み
4. 「順番確認」メッセージに返答
export async function POST(request: NextRequest) {
const body = await request.json();
for (const event of body.events) {
if (event.type === 'message' && event.message.type === 'text') {
const text = event.message.text;
if (text.includes('順番') || text.includes('確認')) {
const waiting = await getWaitingCount(event.source.userId);
await replyMessage(
event.replyToken,
`現在、あなたの前には ${waiting} 人待っています。`
);
}
}
}
return NextResponse.json({ status: 'ok' });
}
ポイント:
- LINE Messaging API の
replyMessageを使用 - キーワードに反応する簡易的な bot
データ構造
Google Sheets の各シート(例: 2026-02-06)
| 番号 | LINE_ID | 氏名 | ステータス | 通知段階 | 登録日時 | 更新日時 |
|---|---|---|---|---|---|---|
| 1 | U123… | 山田太郎 | 完了 | 3 | 2026-02-06T09:00:00 | 2026-02-06T09:30:00 |
| 2 | U456… | 鈴木花子 | 待機 | 2 | 2026-02-06T09:05:00 | 2026-02-06T09:15:00 |
通知段階:
0: 未通知1: あと3人 通知済2: あと2人 通知済3: あと1人 通知済4: まもなく 通知済
セキュリティ対策
1. Webhook 署名検証
GAS と Vercel 間の Webhook に共有シークレットを設定。
const secret = process.env.WEBHOOK_SECRET;
const signature = request.headers.get('x-webhook-signature');
if (signature !== secret) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
2. LINE Webhook の署名検証
LINE からの Webhook が本物か検証。
import crypto from 'crypto';
const channelSecret = process.env.LINE_CHANNEL_SECRET!;
const signature = request.headers.get('x-line-signature');
const body = await request.text();
const hash = crypto
.createHmac('SHA256', channelSecret)
.update(body)
.digest('base64');
if (hash !== signature) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
運用コスト
| サービス | 用途 | 月額コスト |
|---|---|---|
| Vercel | Next.js ホスティング | 無料 (Hobby プラン) |
| Google Cloud | Sheets API | 無料 (制限内) |
| LINE Messaging API | Push 通知 | 無料 (月5000通まで) |
合計: ほぼ 0円 で運用可能
まとめ
できるようになったこと
✅ スタッフの手動通知作業がゼロになる
✅ 患者が LINE で受付・順番確認できる
✅ 段階的な自動通知で待合室の混雑を緩和
✅ 初期コスト・運用コストがほぼゼロ
使った技術
- LINE LIFF でユーザー体験を統一
- Next.js (Vercel) でサーバーレスな API 構築
- Google Sheets をシンプルなデータベースとして活用
- GAS をリアルタイムトリガーとして利用


コメント