BYO-Auth(認証は持ち込み)
Node.js / Express / MySQL

汎用予約カレンダー|導入・運用マニュアル

最短導入・拡張容易・iframe埋め込み対応。管理系は必ず認証で保護してください(Firebase / Supabase / Auth0 / Cognito など)。
*印刷時は背景色なしで読みやすい体裁に切り替わります。

1. 概要

本ドキュメントは、Node.js + Express / 純粋な HTML・CSS・JavaScript で構成された「汎用予約カレンダー」パッケージの導入と運用のための手引きです。 フリーランスや受託開発者の方向けに、セットアップからセキュリティ、日々の運用までを一冊で把握できるようにまとめています。

BYO-Auth(Bring Your Own Authentication):本パッケージには管理者用の認証・ログインは同梱していません。
導入者側の要件に合わせて、Firebase / Supabase / Auth0 / Cognito / 逆プロキシ(Basic+IP制限)等で必ず保護してください。

2. 動作環境・前提

3. 機能ハイライト

4. セットアップ手順

  1. MySQL に sample_calendar データベースを作成し、付属 SQL を流します(全テーブル作成)。
  2. リポジトリを配置し、依存をインストール:
    $ npm i express helmet express-rate-limit mysql2 async-retry express-validator nodemailer xss multer cors node-cron cron dotenv
  3. .env を作成し、MySQL / SMTP / 暗号鍵(ENCRYPTION_KEY)を設定。
  4. 必要に応じて public/ 配下にロゴ等の静的ファイルを配置。
  5. 起動(例):
    $ npm start   # or  node server.js
  6. ブラウザで管理画面と公開画面を確認し、shops / shop_settings に初期データを登録。
本番運用では HTTPSプロセス管理(PM2等)ログ収集WAF/ACL を推奨。

5. .env 設定例

キー 説明
DB_HOST / DB_USER / DB_PASS / DB_NAME MySQL 接続。DB_NAMEは例:sample_calendar
PORT アプリの待受ポート(例:3000
EMAIL_HOST / EMAIL_USER / EMAIL_PASS / EMAIL_FROM_NAME SMTP(TLS/465使用)。通知・テンプレに利用
ENCRYPTION_KEY 32バイト鍵(16進64桁)。顧客氏名/電話/メールのAES-256-GCM暗号化に使用
ADMIN_PURGE_PASSWORD データ初期化API用パス(/notices/purge-reservations)。強固な値必須
NODE_ENV production で最適化
鍵の生成例:openssl rand -hex 32

6. データベース概要

主なテーブルのみ簡潔に。詳細スキーマは付属 SQL を参照してください。

テーブル 用途
shops 店舗基本情報(名称・連絡先・ロゴURL 等)
shop_settings 予約ポリシー(最大予約月数、人数/組数しきい値、TEL切替、幼児カウント 等)
shop_time_slots 曜日別の営業時間(最大3区間、15分刻み)
shop_regular_holidays 第1〜第5など週番号付き定休日
shop_special_days 臨時休業・臨時営業など例外日
reservation_slots 日付×時間スロット。開放状態/満席/TEL等のステータスを管理
reservations 予約本体(暗号化済み氏名/電話/メール、人数、ステータス等)
customers 顧客マスタ(暗号化フィールド・ハッシュ重複防止)
shop_notices キャンセル/違約金/案内文などの可変コンテンツ
seats(任意) 座席/個室の定義(将来の座席指定に備えた拡張)
reservation_logs(任意) 予約操作ログ(生成/確定/取消/更新 等)
notification_logs(任意) 通知履歴(誰に何をいつ送ったか)
restaurants(任意) 将来拡張向けの外部連携プレースホルダ
暗号化カラムは VARBINARY とし、検索には SHA-256 ハッシュ列を併用(重複検知・同一顧客判定の補助)。

6A. ○/△/TEL/満 の判定ロジック

管理画面の 平均滞在時間(minutes)バッファ時間(minutes)アラート閾値(人数/組数)上限(max_seats/max_groups) を用いて、スロットの状態(◯/△/TEL/満)を自動判定します。

6A-1. 基本の考え方(ウィンドウ方式)

優先度:満(full) > TEL(tel) > △(request) > ◯(open) 複数の予約ウィンドウが重なる場合は、上記の優先度でステータスが決まります。

6A-2. 満席時の「周辺スロット」をTELにする(バッファ)

あるウィンドウが 満(full) になった場合、そのウィンドウの前後に「バッファ(分)」を足した範囲のスロットを TEL(tel) に自動変更できます。

6A-3. 用語と設定の対応

概念 設定項目(shop_settings) 説明
平均滞在時間 average_stay_minutes ウィンドウの半径。大きいほど「重なり」やすく、△/満が出やすい
バッファ buffer_minutes 満ウィンドウの前後に追加してTELへ誘導する幅
人数アラート閾値 alert_people_threshold ここを超えたら △(request)
組数アラート閾値 alert_groups_threshold ここを超えたら △(request)
人数ハード上限 max_seats ここを超えると 満(full)
組数ハード上限 max_groups ここを超えると 満(full)
幼児カウント count_infant 幼児(2歳未満)を人数合計に含めるか

6A-4. チューニングのコツ

6A-5. 参考(内部の考え方・擬似式)

# 予約の中心時刻を T(分)とすると
window_start = T - average_stay_minutes
window_end   = T + average_stay_minutes

people_in_window = Σ (大人 + 小人 + [count_infantなら幼児])
groups_in_window = 件数

if (max_seats > 0 and people_in_window >= max_seats) or
   (max_groups > 0 and groups_in_window >= max_groups):
  status = "full"
elif (alert_people_threshold > 0 and people_in_window >= alert_people_threshold) or
     (alert_groups_threshold > 0 and groups_in_window >= alert_groups_threshold):
  status = "request"
else:
  status = "open"

# fullのときはバッファでTEL範囲を追加
tel_zone = (window_start - buffer_minutes, window_start) ∪
           (window_end, window_end + buffer_minutes)
複数予約が重なる場合は「満 > TEL > △ > ◯」の優先度でマージします。

7. セキュリティ実装の要点

8. 運用・日常オペレーション

営業時間・定休日の設定

予約の取り扱い

データの入出力

9. 拡張アイデア

10. トラブルシュート

症状 確認ポイント
予約が確定しない / 満席扱いになる しきい値(max_seats/max_groups)や滞在時間/バッファ、幼児カウント設定を確認。
CSV が文字化けする Excel での読み込み時に UTF-8 を選択。BOM 付き出力なので通常はそのまま開けます。
画像ロゴが反映されない MIME が PNG/JPEG/GIF か、1MB 以下かを確認(multerの制限)。
メールが届かない SMTP 設定、送信ドメイン認証(SPF/DKIM/DMARC)、迷惑メール判定を確認。
時刻ずれが起きる サーバ/DB のタイムゾーン設定(DBは Asia/Tokyo を目安)。
iframe の高さが合わない postMessage の導線と origin 検証を再確認。

11. FAQ

Q. フロントはフレームワーク不要ですか?

はい。純粋な HTML / CSS / JS で動作します。必要に応じて任意のフレームワークに差し替え可能です。

Q. 多店舗運用時の注意は?

営業時間や定休日の差異がある場合、shop_settings と各種マスタで店舗単位の設定を維持してください。

Q. 個別要件の実装は難しい?

ベース機能は揃っており、AI を活用すれば「色替え」「外部連携」などの改修は比較的容易です。

12. 既存サイトへの埋め込み(iframe)

基本スニペット(origin検証あり)

<div style="max-width:680px;margin:0 auto;">
  <iframe
    id="reserveFrame"
    src="https://あなたのドメイン名/reserve-calendar.html?shop_id=1&embed=1&parent_origin=https%3A%2F%2F埋め込み先のドメイン"
    title="予約カレンダー"
    style="border:0;width:100%;height:720px;display:block;background:transparent"
    loading="lazy"
    referrerpolicy="no-referrer-when-downgrade"
  ></iframe>
</div>

<script>
(function () {
  const frame = document.getElementById('reserveFrame');
  const ALLOWED_ORIGIN = 'https://あなたのドメイン名'; // ← カレンダー配信元の正しいオリジンに固定
  const MIN_H = 320, MAX_H = 5000;
  let rafId = null;

  window.addEventListener('message', function (e) {
    // セキュリティ:想定オリジンのみ許可
    if (e.origin !== ALLOWED_ORIGIN) return;
    if (!e.data || e.data.type !== 'setIframeHeight') return;

    const h = Math.max(MIN_H, Math.min(parseInt(e.data.height, 10) || 0, MAX_H));
    if (rafId) cancelAnimationFrame(rafId);
    rafId = requestAnimationFrame(() => { frame.style.height = h + 'px'; });
  });

  // 任意:ロード後に子側へ「初期化」通知(子が高さを計測して送り返す設計)
  frame.addEventListener('load', () => {
    try { frame.contentWindow?.postMessage({ type: 'reserve:init' }, ALLOWED_ORIGIN); } catch (_) {}
  });
})();
</script>

埋め込み先(子)からの送信(同梱フロントで対応済)

// 親へ高さを通知
window.parent.postMessage({ type:'setIframeHeight', height: document.body.scrollHeight }, '*');
公開サイト側は postMessageorigin を必ず検証してください。

13. 主なAPI(抜粋)

管理系(要認証・要アクセス制限)

公開系(一般利用)

エンドポイントの公開/非公開は導入構成により変化します。公開が必要な最小限のみ外出しし、他は認証の背後へ。

14. CSV出力

顧客・予約ともに UTF-8 + BOM、全セルをダブルクォートで出力。Excel/スプレッドシートでの文字化け・数式化を防ぎます。

CSVエンドポイントは管理用途です。公開しないでください。

15. 多店舗運用のポイント

16. ライセンス/サポート


© 2025 汎用予約カレンダー(BYO-Auth版)