全国オンライン対応受付 平日 9:00-18:00
お問い合わせ

開発記録 / toi-toi-toi制作記 / Vol.31 toi-toi-toi制作記 Vol.5

記事 02

イベント管理をゼロから設計した——新しいシートと新しい申込みフロー

「体験セミナーや講座をイベントとして管理したい。定員があって、満員になったら受付を止めたい」

2026-06-13 公開

「体験セミナーや講座をイベントとして管理したい。定員があって、満員になったら受付を止めたい」

これまでの予約システムは「サービス種別ごとに枠を作る」設計だった。コーチングのレッスン、相談、ハウスツアー。どれも1対1か少人数が前提で、枠ごとに1件の予約が入る形だ。

イベントは違う。複数の申込者が同じ日時に集まる。定員がある。場合によっては決済リンクが必要で、画像も使いたい。既存の枠管理の仕組みでは対応できなかった。


新しいシートを2つ作った

既存の「予約枠」「予約」シートとは独立した設計にした。

イベントシートの列構成:

event_id / name / description / date / time / capacity
/ booked / price / payment_url / image_url / zoom_url / status / created_at

イベント予約シートの列構成:

booking_id / event_id / event_name / date / time / name / phone / email
/ line_user_id / status / notes / created_at / paid / pay_remind_sent_at / pay_confirm_sent_at

最初からpay系の列を持たせた。あとから追加すると列番号がずれて既存データが壊れる。


管理画面にイベント管理ページを追加した

管理者がイベントを作成・編集・削除できる画面を作った。

各イベントには「申込者を見る」ボタンがある。クリックするとモーダルが開き、申込者一覧が表示される。キャンセルもここから実行できる。

受付状態の切り替えは「募集中 / 受付終了」の2値で管理した。管理者が手動でボタンを押して切り替える。定員到達は capacitybooked の比較でシステム側で制御するが、イベント自体の公開/非公開はオーナーの判断に委ねる設計だ。


フロントエンドに申込みフローを追加した

/reserve?type=event というURLでイベント申込みモードに入る。

通常の予約フローとは別に、ステップを設けた。

  1. イベント一覧を表示する(募集中・未来のものだけ)
  2. イベントを選ぶ
  3. 氏名・電話番号を入力する
  4. 完了画面に決済リンクボタンを表示する

通常予約との最大の違いは、同時に複数の申込みが重なったとき満員を超えないようにすることだ。


LockServiceで競合を防いだ

「定員5名のイベントに同時に6件申込みが入ったとき」というシナリオを考えた。

GASはサーバーレスに近い実行モデルで、複数のリクエストが並走する可能性がある。カウントを読んで書くまでの間に別のリクエストが入ると、定員オーバーが起きる。

LockService.getScriptLock() を使って排他制御した。

const lock = LockService.getScriptLock();
try { lock.waitLock(15000); }
catch(e) { return jsonResponse({success: false, error: 'サーバーが混み合っています。しばらくしてからお試しください'}); }

const currentBooked = parseInt(evSht.getRange(evRowIdx, 7).getValue()) || 0;
if (currentBooked >= evCapacity) {
  lock.releaseLock();
  return jsonResponse({success: false, error: '申し訳ありません。このイベントは満員です'});
}

ロック取得後に再度カウントを読む。ロック前に読んだ値は信頼しない。

registerBooking() でも同じ設計にしている。並走問題は仕組みとして解決するしかない。


申込み完了後に決済リンクを表示した

有料イベントの場合、申込み完了画面に決済ボタンが出てほしい。

URLをどこで持つか。イベントシートの payment_url 列だ。申込みフローの途中で選んだイベントのURLを evState.eventPayUrl に保存しておき、完了後にボタンとして挿入する。

const pbEl = document.getElementById('e-payment-btn');
if (pbEl) {
  pbEl.innerHTML = evState.eventPayUrl
    ? `<a href="${esc(evState.eventPayUrl)}" target="_blank" rel="noopener"
         style="...">💳 お支払いはこちら</a>`
    : '';
}

URLがなければボタンは出ない。無料イベントはそのままでいい。


既存システムに「イベント」という概念を追加する作業は、テーブル設計から始まった。設計が決まれば実装は続く作業だ。GAS側のAPI群、EC2のプロキシ設定、管理画面、予約フォーム。ひとつの機能を動かすまでに動かすものが多い。

次回は、入金管理を作った話を書く。

*シンプルシステム株式会社 代表 伊藤勝彦*