本記事は生成AIと共同で執筆しています。事実関係は可能な範囲で公式ドキュメント等と照合していますが、誤りが含まれている可能性があります。重要な判断を行う前にご自身でも一次情報をご確認ください。

会議室や備品(プロジェクター、貸出ノートPCなど)の予約を、URL を共有するだけで使えるようにする Web アプリを作りました。予約する人にはアカウントを求めず、管理者だけが予約表を作成・管理します。操作感は一般的なカレンダーアプリに寄せ、日・週・月の 3 ビューと、空き時間を縦にドラッグして予約する方式を採用しています。

URL を配れば使える予約ツールの手軽さと、カレンダーアプリのような一覧性を、1 つの画面で両立させることを目標にしました。

デモ

Google アカウントでログインすると予約表を作成でき、発行された共有 URL(/s/{token})を配ると、その URL を知っている人はログインなしで予約できます。以下の画面はサンプルの予約表です。

予約ボードのランディングページ。ヒーローの下にお知らせ、その下に機能紹介のカードが並ぶ。

画面

週表示

既定は週表示で、その予約表に登録した全項目(会議室・備品など)を 1 週間分まとめて重ねて表示します。項目ごとに色が付き、左サイドバーのチェックボックスで表示・非表示を切り替えられます(設定はブラウザに保存されます)。同じ時間帯に複数の予約が重なる場合は、横に並べて配置します。

週表示。会議室A・B・C、プロジェクター、貸出ノートPCの予約が色分けされて 1 週間分表示されている。

空いている時間帯を縦にドラッグすると予約フォームが開き、予約する項目・開始/終了の日時・予約者名・用途を入力します。開始日と終了日を別々に指定できるので、日をまたぐ予約(宿泊や連日利用)にも対応します。既存の予約をクリックすると編集・削除できます。

日表示・月表示

日表示は 1 日を大きく、月表示は 1 か月を俯瞰します。月表示では各日にその日の予約が色付きのチップで並び、日をクリックするとその日の日表示へ移動します。日曜と祝日は日付を赤で表示します(祝日は内閣府が配布する CSV から生成しています)。

月表示。各日に予約の件数と項目がチップで表示され、当日がハイライトされている。

予約ページはフッターを出さず、ヘッダーを除いた領域いっぱいにカレンダーを表示します。縦スクロールはカレンダー内部だけで起き、ページ自体はスクロールしません。日付の見出し行はスクロールしても上部に固定されます。

日表示。1 日分の時間軸に予約が大きく表示されている。

画面幅が狭いときは左カラムとカレンダーを縦に積み、通常のスクロールに切り替えます。

スマートフォン幅でのランディングページ表示。

capability URL による共有

このアプリの中心にあるのが capability URL(ケーパビリティ URL)です。予約表ごとに推測困難なランダムな token を発行し、/s/{token} という URL を知っていること自体をアクセス権とみなします。URL を配られた人はログインせずにその予約表を見て予約でき、URL を知らない人はアクセスできません。

capability URL の流れ。管理者が予約表を作成して URL を発行し、利用者はログインなしで予約する。

アカウントで保護するのは予約表の作成・削除や課金といった管理操作だけで、日々の予約はアカウント不要にしています。URL が漏れたと分かったときは、管理画面から token を再生成すると古い URL は無効になります。

予約を保存するときは、同じ項目の同じ時間帯に他の予約が入っていないかをデータベース側でチェックしてから書き込み、重複予約を防いでいます。予約は開始日時から終了日時までの区間として扱い、日をまたぐ予約は各日に切り出して表示します。

1 つのソースコードで 2 つのエディション

このアプリは、学内サーバに置く内部版と、インターネット上で公開する SaaS(Software as a Service)版の 2 通りの使い方を想定しています。両者はストレージ・認証・課金の実装だけが異なり、ソースコードは共通です。どの実装を使うかは環境変数で選びます。

1 つの src(Next.js 16)を環境変数で切り替える構成図。内部版は sqlite・パスワード認証・課金なしで学内サーバ(aptest)へ、SaaS 版は Cloudflare D1・Google 認証・Stripe 課金へと分岐する。

ストレージと認証は、実装をインターフェースの裏で差し替えられる形(リポジトリパターン)にしています。どの実装を使うか、そして課金を使うかどうかは、次の 1 か所で環境変数から選びます。

export function resolveDrivers(env = process.env) {
  return {
    storage: oneOf(env.YOYAKU_STORAGE, ['sqlite', 'firestore', 'd1'], 'sqlite', 'YOYAKU_STORAGE'),
    auth:    oneOf(env.YOYAKU_AUTH,    ['password', 'google'],        'password', 'YOYAKU_AUTH'),
    payment: oneOf(env.YOYAKU_PAYMENT, ['none', 'stripe'],            'none',     'YOYAKU_PAYMENT'),
  };
}

内部版は次のような設定で、SQLite に保存し、パスワード認証で、課金は使いません。

YOYAKU_STORAGE=sqlite
YOYAKU_AUTH=password
YOYAKU_PAYMENT=none

SaaS 版は Cloudflare D1(SQLite 互換のマネージド DB)に保存し、Google ログイン、Stripe による課金を有効にします。同じ画面・同じ機能のまま、YOYAKU_PAYMENT=none にすれば課金の導線は表示されません。ストレージは SQLite と D1 でほぼ同じ SQL を使うため、2 つのドライバの差分は小さく収まっています。

内部版は Node.js の組み込み node:sqlite を使い、生成物を学内サーバへ rsync で配置します。SaaS 版は Cloudflare Workers 上で動かし、OpenNext(@opennextjs/cloudflare)で Next.js をビルドしてデプロイしています。

その他の機能

  • お知らせ: 運営者だけがトップページのお知らせを投稿でき、トップに最新数件、/news に全件を新しい順で表示します。

お知らせ一覧ページ。日付と本文が新しい順に並ぶ。

  • iCal フィード: 予約表ごとに /api/ical/{token} で ICS(iCalendar)フィードを配信し、カレンダーアプリから購読できます(購読側の都合で反映は数時間ごとで、リアルタイムではありません)。
  • CSV 入出力: 予約を CSV で書き出し・取り込みできます。
  • 繰り返し予約: 毎日・毎週の繰り返しで複数の予約をまとめて作成します(重複する回はスキップします)。
  • 印刷: 印刷時はサイドバーやダイアログを隠し、カレンダーだけを出力します。
  • メール通知: 予約が入ると、予約表の所有者にメールで通知します(送信は Resend を利用、環境変数が設定されているときのみ有効)。

技術スタック

  • Next.js 16(App Router)/ React 19 / TypeScript
  • next-intl(日本語・英語)/ Tailwind CSS
  • ストレージ: node:sqlite(内部版)/ Cloudflare D1(SaaS 版)
  • 認証: 自前実装のパスワード認証 / Google の OpenID Connect(OIDC)
  • 課金: Stripe(サブスクリプション、月額・年額)
  • メール: Resend
  • デプロイ: 学内サーバへ rsync / Cloudflare Workers(OpenNext)

日々の予約はアカウント不要で URL 共有だけで回し、管理と課金だけをアカウントで守る、という切り分けにしたことで、利用者の導入コストを小さく保てました。1 つのソースコードを環境変数でエディション切り替えする構成は、学内向けと一般公開向けを別々にメンテナンスせずに済む点で扱いやすいと感じています。