文化祭入退場・混雑管理システム "CAPPUCCINO" 開発・運用記

2021/11/24に公開
3

この記事は、文化祭実行委員会に所属する[1]現高校 3 年生 3 人で文化祭の入退場混雑管理システム「CAPPUCCINO」を 2 年間に渡り開発し、実際に運用した記録を残したものです。

*この記事は以下の 3 名による共同執筆です

著者・開発者:

  • すばる (@su8ru / Twitter:@su8ru_)
    高校3年生。普段は TypeScript に使われながらウェブフロントエンド技術で遊んでいます。
  • たけ (@takeno_hito / Twitter:@Takeno_hito)
    こうこうさんねんせい。コーディングよりはゲームのほうが好きです。上下二人に揉まれながらも一応リーダーをやってました。
  • ふわわあ (@fuwa2003 / Twitter:@ibuki2003)
    高3。Vimmer。半言い出しっぺなのに後半コード書いてないとかで最近は肩身が狭い思いをしています。

https://twitter.com/su8ru_/status/1459880972879466501

https://github.com/afes-website

1. CAPPUCCINO システム概要

私達が開発した CAPPUCCINO は、文化祭の開催において入退場を管理し、混雑を管理することを目的としたシステムです。今年の 11月13日と14日に開催された第74回麻布学園文化祭で実際に運用されました。

1-1. 主な機能

CAPPUCCINO の主要な機能は以下の 5 つです。

  • 配布した予約チケットの QR コードから予約情報を照会し、それと紐付けたリストバンドを着用させ来場者の滞在状況を管理する。
  • 来場者が展示教室に入室する際にリストバンドを提示し、展示員がその QR コードをスキャンする事で各展示教室の滞在人数を常に把握し、また上限を超えないよう管理する。
  • 校内の全展示教室の滞在人数を一覧し、確認できるようにする。
  • あらかじめ来場者ごとに滞在可能な時間帯を設定する事で、来場者の入場・退場時間帯を分散させる。
  • 来場者の行動履歴を記録し、万が一陽性者が発生した際の濃厚接触者の特定・情報提供をスムーズに行えるようにする。

入場口での入場処理の様子
入場口での入場処理の様子

1-2. 運用の流れ

入退場口

予め来場者に配布したデジタルチケットを提示してもらい、係員が受付の iPad で予約 QR コード・リストバンド QR コードをスキャンし、バックエンドでリストバンドを予約と紐付けてデータベースに登録することで、入場処理を行います。

各展示の出入り口

各展示員がアプリでリストバンドをスキャンすることによって入退室を記録し、各来場者の滞在状況を更新します。これにより、本部や文実構成員が混雑状況をリアルタイムに把握できるようになります。

1-3. ネーミング

当初のプロジェクト名は manage-app でしたが、COCOA(厚生労働省)および UTokyo MOCHA(東京大学)と親和性の高い言葉として、同じく飲み物繋がりで「CAPPUCCINO(カプチーノ)」と命名しました。

2. CAPPUCCINO を支える技術

2-1. アプリ(フロントエンド)

PWA

ネイティブアプリ案と PWA (Progressive Web Apps) 案がありましたが、最終的には React(React DOM) で PWA として配布する形式を取りました。
iOS / iPad OS での動作などにやや不安がありましたが、技術検証を繰り返して問題ないと判断しました。
(そもそも iOS / iPadOS ネイティブアプリの開発コストがとても高いという障壁もありました)

https://twitter.com/su8ru_/status/1286199465024434178

React (create-react-app)

前年度の構成

前年度の公式ウェブサイトでは「当時盛り上がってて実用に耐えそうだった」という理由で Vue.js v2 を採用しました。後に TypeScript との相性が微妙であることが発覚してちょっと大変でした……
UI コンポーネントライブラリには「Bootstrap は使ったことがある」という理由で BootstrapVue を採用しました。

今回の構成

アプリでは、開発開始当時(2020年7月)話題になっていた Composition API を採用しようとも考えましたが、それを搭載した Vue 3 が正式リリース前(2020年9月18日正式リリース)ということで、2019年2月に正式リリースされ情報が多い React の採用を決めました。

これに伴い、「普段から見慣れている UI」「コンポーネント数が多い」などの理由から、Material UI v4 を採用しました(当時は MUI v5 正式リリース前)。

  • サーバー: ConoHa WING(バックエンドと共有)
  • 言語: TypeScript
  • UI ライブラリ: React (React DOM)
  • UI コンポーネントライブラリ: Material UI (v4)

状態管理はそれほど複雑になる予定がなかったため、Redux や Recoil などは使用せずに Context を使用しています。

aspida

前年度の公式ウェブサイト開発時から使用している TypeScript 製 REST API クライアントです。
各エンドポイントに型定義をしてコンパイルで生成される対応メソッドを叩く、という変わったクライアントで、この構造を逆手に取って定義ファイルたちを API 仕様書としました。

以前は Swagger を API 仕様書とし、フロントエンドには別途型定義を書いていたのですが、型の柔軟さに欠けることや 1 ファイルが膨れ上がって見通しが悪くなることに耐えられなくなったため、アプリ開発開始時に aspida 定義に一本化、ES modules として npm パッケージ化(後述)し、複数のフロントエンドリポジトリから読み込んで使用できています。

https://github.com/aspida/aspida

2-2. サーバー(バックエンド)

昨年の公式ウェブサイトの開発時、次の要件を条件に技術選定を行いました。

  • 枯れた技術であること
    • 信頼性と情報の多さは開発・運用の安定性に直結すると考えています
  • サーバーが安価に借りられること
    • 限られた予算の中で運用する必要がありました
    • フロントエンドの serve と共有できる、Web サーバーレンタルサービスを選ぶことで解決できると考えました
  • チームメンバーの複数が経験していること
    • 学習と同時に開発をすると作法がわからないままコードを書くことになりメンテナンス性の低下につながるので、それは避けたいと考えました

結論として、以下の構成に至りました。

  • サーバー: ConoHa WING(フロントエンドと共有)
  • 言語: PHP
  • フレームワーク: Lumen
  • データベース: MySQL

その開発段階後期で感染症拡大をふまえて入退場管理のシステムを開発する案が浮上し、はじめはウェブサイトと CAPPUCCINO を同じシステムで動かそうとしていたため、そのままプロジェクト独立後の本システムでも同様のシステム構成としました。

https://twitter.com/ibuki2003/status/1458967239059664897

2-3. API 仕様の管理

前述の通り、前年度の公式ウェブサイト開発開始時は Swagger にて管理していたのですが、ウェブサイトのフロントエンドでは当初から aspida を使用しており、TypeScript の型表現の恩恵を享受するためにも API ドキュメント自体を aspida の記法で記述するようにしました。
各種説明や aspida のカバー対象外であるエラーについては TSDoc を用いて記述し、build した上で npm 上に公開しています。

https://www.npmjs.com/package/@afes-website/docs

2-4. ソースコード管理

普段からメンバーが使用しており、無料で十分に強力な環境が用意できるということから、Git, GitHub を採用しました。

ブランチ運用は Git flow と GitHub flow が候補にありましたが、レビュー人員が十分ではなくデプロイ頻度を調整するため、master と develop を分け Git flow を採用しました。
(API 定義を管理する docs についてはその必要がなく、GitHub flow を採用しています。)

CI/CD

いずれも GitHub Actions を使用しています。無料でここまで使えることにただただ感謝。

フロントエンドは Storybook などを導入できずに build / linter のみのテストですが、バックエンドには PHPUnit を用いたユニットテストを導入しています。

API 定義は GitHub Actions で npm publish していますが、event を tag push に設定しているため、tag push 忘れなどの事故が度々発生しました。

リポジトリ構成

フロントエンドとバックエンドが密接に関わっていることから monorepo 構成にする案もありましたが、最終的に次の理由からバックエンド、フロントエンド、API ドキュメント(aspida 型定義)の 3 つにリポジトリを分けることにしました。

  • 開発メンバーが分離されていること(当初両方を書ける人が 1 人だけだった)
  • 開発メンバーの誰も monorepo 構成をやったことがなかったこと
  • monorepo 構成にしたときにデプロイ等が面倒であったこと
  • 完全に同期されたバージョン管理が難しかったので、コミットツリーが煩雑になる恐れがあったこと
  • API ドキュメントの更新後に両サイドでそれに準拠した開発というサイクルを確立するため

これらのリポジトリはすべて以下の GitHub Organization にあり、そのほとんどを MIT License として公開しています。

https://github.com/afes-website

3. システム全体の設計と仕様

3-1. CAPPUCCINO システム全体の流れ

予約・抽選段階

  1. 教員が予約受付・抽選および予約 ID の発行を行い、私たちに予約 ID と時間帯情報を渡します。(個人情報はすべて教員管轄になっているためです)
    • 予約 ID の形式は、サーバーが停止するなどの万が一を想定して、人の目でも確認できるように R-ABCNNNX という形式にしました。
      • R- : 予約 ID の prefix
      • A : 1 日目 / 2 日目
      • B : 時間枠番号(午前が 1~3, 午後が 4~6)
      • C : 人数
      • NNN : 連番 3 桁
      • X : 乱数 1 桁
    • チケット URL の末尾をいじって他のチケットを入手しようと試みる人は一定数いましたが(残念ながら)対策済みでした。
  2. 私たちは受け取ったデータをデータベースに登録し、教員はチケットの URL 付きのメールを送信します。
  3. 予約者はチケットのリンクを開くと予約情報等が確認できる仕組みです。
    3-4. デジタルチケット
  4. 保護者用の入場券は紙で配布するので、入場券には時間帯ごとに共通の QR コードを印刷します。

入場受付

  1. 入場開始前に、来場客に対して予めチケットを表示してもらうようにアナウンスします。(入場口で詰まる事をできるだけ防ぐためです。)
  2. 来場客に、所定の場所にスマホ・入場券を置いてもらって QR コードをスキャンします。
  3. バックエンドで入場可能かどうかを検証します。
  4. チケット・アプリ上で渡すべきリストバンドの色や枚数が表示されるので、その色のリストバンドの QR コードを必要数アプリでスキャンして、来場客に渡します。
    • リストバンド ID の形式は AA-BBBBX のようにしました。
      • AA : リストバンドの種類
      • BBBB : 16 進数の乱数
      • X : チェックサム;手入力で登録する際に、間違えたリストバンドを登録していないかを確認するためのものです。
  5. リストバンドをスキャンするたびにリクエストを送り、データベースに予約 ID とセットでリストバンドを登録します。滞在可能時間帯は予約情報をもとに設定します。

各展示での入退室

  1. 来場者は入口の展示員にリストバンドを提示して、展示員がそれをスキャンします。
  2. バックエンドで展示の滞在状況・来場者の入場時間帯などの情報を確認します。問題がなければ来場者の滞在場所情報を更新して、また行動履歴のテーブルに情報を書き込みます。
  3. 展示員はアプリに表示された可否を確認して、それに応じて対応します。
  4. 満室時には警告が表示されるようになっています。
  5. 退室についても、出口の展示員が同様に行います。

紛失対応

紛失対応の係員が行う動作・処理は紛失対応の画面であること以外は入場時と同様です。
予備のリストバンドは、データベースには is_spare フラグを設定して登録します。予備リストバンドは入場処理時の「その予約で既に入場している人数」にはカウントせず、「滞在者数」としてはカウントします。
紛失したリストバンドについての辻褄合わせは退場時に行います。

退場処理

来場者はリストバンドをスキャンするだけです。
当日は、急遽来場者が全員退場するタイミングではスキャンを不要とすることになったので、データベースを直接書き換えて対応することになりました。

紛失したリストバンドの退場処理は以下のようにしました。

  1. 予約情報に紐付いている来場者が全員退場しているかをチェックします。
  2. もし入場処理を済ませた数と同じ人数分退場していれば、残っているリストバンドを「紛失したリストバンド」と断定しそのリストバンドを退場扱いにします。

滞在状況管理

校内の全展示教室の滞在一覧を一覧し、確認できるようにしました。
(当日教員も使っていたそうで、役に立っていたらしいです。具体的にどのように使われていたかはよく知らないのですが…)

一覧については、マップ表示と一覧表示の 2 種類を用意しました。一覧表示では、「展示番号順」「滞在人数順」「混雑度順(=空き人数順)」の 3 種類でソートできるようにしました。
混雑度順で割合ではなく空き人数を用いているのは、実際にどれくらい混雑しているかよりも「満員になりそうか」のほうが有用だと判断したためです。満員時に教室の外に待機列ができてしまうなど、展示団体だけではどうにもならない事態に備えてのものです。

行動履歴追跡

「入場」「退場」「展示入室」「展示退室」「予備登録」の記録をすべて行動履歴テーブルに逐一書き込むことで、文化祭開催後に万が一陽性者が発生した場合に濃厚接触者の特定・関係各所への情報提供をスムーズに行えるようにしました。

その他

  • 認証にはサーバーサイドでの状態管理が楽であるという理由だけで JWT を採用しました。
    後にトークンの無効化の需要が発生しましたが、ユーザごとに session_key として文字列を用意し一致判定するという力技で乗り切りました……
  • 当初は個人情報も私たちが管理する事を想定していたので、予約情報を一般生徒に見せないようにして滞在履歴と個人情報を分離するために、入口で予約 ID と来場者 ID を紐付けるとき以外では予約 ID に関与しないようにした上で予約情報を閲覧するのに別途権限を必要とするようにしました。
  • 特に guest/check-in などで、エラーのレスポンスにエラーコードを加え、失敗した原因をフロントエンドで表示するようにしました。当日かなり役に立ちました。

3-2. アプリの機能

アプリの画面サンプル
アプリの画面サンプル

UI

前述の通り UI コンポーネントライブラリに Material UI を使用しているため、全体として Material Design に統一しています。

また、入場処理画面では来場者が提示するデジタルチケットがそのまま受付端末に表示されたり、アカウント一覧は Twitter の UI に寄せるなど、初めて使う場合でも直感的に操作できるよう心がけています。

アプリの入場処理画面
アプリの入場処理画面

PWA

前述の通り、この「アプリ」は PWA (Progressive Web Apps) で実装しました。そのため、インストール手段がやや複雑になっています。
「アプリはアプリストアから入れるもの」という認識を持っているであろう展示員がどの程度理解してインストールしてくれるのかは未知数でしたが、以下の資料や案内によって概ねインストールできていたように思います。
Apple は Chrome から PWA をインストールできないのをどうにかしてくれ……

「インストールガイド」を作成し各展示に配布
「インストールガイド」を作成し各展示に配布

アプリをブラウザで開いた場合もインストールを促す
アプリをブラウザで開いた場合もインストールを促す

QR コードログイン

スムーズにすべての展示員がアカウントにログインできるよう、展示ごとにログイン用の QR コードを印刷し配布しました。

といっても所詮は JWT ベースの認証管理なので、ID と password を入れた JSON を Base64 にしただけのお気持ち実装です。
目的は ID / PW の流出防止ではなくあくまでログインの利便性向上だったので。

R.I.P. 混雑状況ヒートマップ

読んで字の如く、展示教室の滞在人数をヒートマップで色別表示するやつです。

実は目玉機能だったんですが、各所との(事務的な)調整不足でポケット Wi-Fi が用意できず、一般公開できませんでした。かなしい……
ちなみに各所にあったアナログ混雑度表示はたぶんこれを元に手作業で更新されていたのだと思います。

実際のヒートマップのスクリーンショット
実際のヒートマップのスクリーンショット

R.I.P. 待機列予約チェック

入場処理とは別に、予約 QR コードから有効性を確認できるページがあり、当初の計画では入場待機列に並んでいる来場者に対して予め予約チェックをすることで、入場受付での詰まりを防止する予定でした。

スペース・人員の不足により断念しましたが、結果としてはそもそも入場処理がかなりの速度で行われていたため問題ありませんでした。

3-3. エンドポイント一覧とオブジェクト型定義

エンドポイント一覧
├── auth
│   ├── login:POST                    id と pw でログイン
│   ├── me:GET                        わたしはだれ
│   └── users:GET
│       └── _id@string:GET            ユーザー情報
│           ├── change_password:POST  パスワードを変更
│           └── regenerate:POST       session_key を再生成
├── exhibitions:GET                   展示状況 + 校内全体の滞在状況
│   └── _id@string:GET                展示情報・滞在状況
├── guests:GET
│   ├── _id@string:GET                来場者情報
│   │   ├── check-out:POST            文化祭から退場する
│   │   ├── enter:POST                展示に入室する
│   │   └── exit:POST                 展示から退室する
│   ├── check-in:POST                 文化祭に入場する; 予約情報と紐付けて登録
│   └── register-spare:POST           予備リストバンドを登録する; 予約情報と紐付け
├── images:POST                       画像のアップロード
│   └── _id@string:GET                取得
├── log:GET                           来場者の行動履歴取得
├── reservations
│   ├── _id@string:GET                予約情報
│   │   └── check:GET                 入場可能な状態かのチェック
│   └── search:GET                    検索
└── terms:GET                         ターム情報一覧
各オブジェクトの型定義
type Guest = {
  id: string;
  registered_at: string;
  revoked_at: string | null;
  is_spare: boolean;
  exhibition_id: string | null;
  term: Term;
}
type ActivityLog = {
  id: string;
  timestamp: string;
  guest: GuestSummary;
  exhibition_id: string;
} 
type Reservation = {
  id: string;
  term: Term;
  member_all: number;        // 予約人数
  member_checked_in: number; // すでに入場済みの人数
}
type Term = {
  id: string;
  enter_scheduled_time: string;
  exit_scheduled_time: string;
  guest_type: GuestType;
  class: GuestClass;
}
type Status = {
  all: {
    count: {    // 現在の入場者の数
      [term_id: string]: number;
    }
  },
  exhibition: {
    [exh_id: string]: {
      info: exhibitionInfo;
      count: {...}
    }
  }
}

3-4. デジタルチケット

文化祭に入場するためのチケットは、在校生の保護者には紙の入場券、一般入場客にはデジタルチケットをメールで送信する形式としました。

チケットの画像をメールに添付するのはキャリアメール等を考えるとあまり得策ではないため、ウェブ上でチケットを表示できるようにした上で URL を添付し、スマートフォンの画面で表示するか予め印刷し持参してもらいました。

仮に文化祭当日にネットワーク環境が悪い場合でも問題なく表示するため、ServiceWorker を用いて SPA 全体をキャッシュし、Reservation の json を localStorage に保存することで、事前に一度開いてあればオフラインでも問題なくチケットを表示できる仕組みとしました。

なお、文化祭前日までに 7 割強の人がチケットを表示してくれていました。

デジタルチケットのスクリーンショット
デジタルチケットのスクリーンショット

4. 開発・運営記

時系列順にできる限り書き起こしていきます。

4-1. 本格的な開発に至るまで

このプロジェクトの原案は 2 年くらい前まで遡ります……

2019/10: 73代(前年度) 文実が発足

73 文実の委員長に「公式ウェブサイトを作成しないか?」と頼まれて、公式ウェブサイトを製作することになります。→ https://73.afes.info
このとき、例年数万人が訪れる本校文化祭の混雑を解消させるために、この時は各展示で配られる整理券配布を一元化する事を目的とした最初の管理案を思いつきます。が、73 文実に難色を示されて廃案に。

「動かなくなった時どうするの?」--- 統制局長

2020/03~10: 新型コロナウイルス感染拡大

学校はオンライン授業になり、73 文化祭は延期。この時に現在の「CAPPUCCINO」システムを思いつき、6 月から実装を始めます。73 文化祭の開催に向けて必死に製作しますが、感染状況も非常に厳しく「保護者と生徒のみ」で開催することに。こうなってしまうとわざわざシステムを使う理由は全く無いので頓挫、73 文化祭での運用を諦める事になりました。

「生徒の登校と生徒自治活動など校内外での生徒諸活動を禁止します」--- FairCast 2020.04.03

2021/02?: 後輩の打診

不完全燃焼の中、後輩の 74 文実から打診が来ます。打診の詳細と経緯をあまり覚えていませんが、もう一度チャレンジする事を決めます。既に私達は高 3 直前なのでどうにか受験勉強と両立しながら頑張っていくことになります。

「高 3 の秋を 1 週間くらい使う覚悟とか人選とか?しておいてほしい」--- 社会科K先生

4-2. 設計・実装

2021/02~07: 文実・教員との相談・交渉

2 月から何度も教員や後輩と話しながらどんどん設計を進めます。「予約枠はどうするのか」「外部客の個人情報を生徒が管理していいわけがない」「QR コードの認知度は十分なのか」「感染者が出たときにどのように対応するのか」「そもそも一般的な麻布生はシステムを正しく運用できるのか」などなど……

去年途中まで進めたシステムを、文実がどのように扱いたいかや教員が何を問題視しているのかを聞きながら、一つ一つ説明したり解決したりを続けていきます。

2021/02~10: 実装

フロントエンド

詰め込みたい機能を全部詰め込んだ結果、実装量が大変なことに…。
基本的には Material UI のコンポーネントをどんどん使っていたためあまり書くことはありませんでしたが、大規模なスタイルを書く際に CSS in JS を書くのが苦痛でした。
それでもどんどんコードを書いて UI を作り上げていく過程はとても楽しかったです。

バックエンド

(API 仕様と同時にバックエンドの仕様もだいたい決めていたので)実装自体はそんなに時間もかからずに終わりましたが、TDD について軽く説明を受けたもののテスト駆動開発をよく理解せずに実装していたので「実装」「リファクタリング」「テスト」の順で実装してました。どのようなものかを調べて TDD の目的を知ったのは文化祭 1 週間前。ごめんなさい。

2021/10: オフライン対応の要求

どうやら本校の教員は文化祭のシステムの根幹を生徒が握るのを嫌がるようで(文化祭は学校行事でもあるから失敗したくないらしい)、「万が一回線の混雑などが起きてもせめて入場処理がトラブらないようにしてほしい」という要求が教員側からありました。
これくらいならまあそんなに面倒ではないだろうと考えて承諾。

しかし、いざ実装しようとしてみると(主にアプリで)大量の問題が発生し、大きく苦労することになります……

2021/10: 文実・教員への説明

リストバンドのサンプルが届き、文実と教員に実物を見せながら説明します。
以前から何度か説明をしてはいたものの、実物が無かったことからかあまりイメージを掴めていなかったようで、この時に初めて良い感触を得ることができました。口頭・文章・画像などで説明するだけでは理解してもらうのには不十分だということを学ぶ機会になりました。

また、入場口でのスキャンのイメージを文実と共有できたので、このタイミングで入場口の流れが確定しました。

教室での入場処理イメージ共有の様子
教室での入場処理イメージ共有の様子

2021/10~11: 予約チケットの準備

根幹を握るのを嫌がれながらも、情報科教員の後押しで予約チケット画面を私たちで用意することに。
実はこの先生、就任 2 年目という早さで本校の自治などに大きく関わる生徒委員会に所属し、麻布生のイタズラ癖をまだよく知らないから 割と新しいことを許してくれる方だったので助かりました。

4-3. 文化祭準備から当日まで

11/1(12日前): 最初の講習会の開催

リストバンドの実物をお披露目しました。
当日入退場口を実際に担当する局員と、展責・展示担当の局員を招集してもらい、使い方を一通り説明しました。アプリを実際に見せた時に「すごい」という声を多数聞けてとても嬉しかったです。

上から順に当日用、テスト用、保護者・生徒用
上から順に当日用、テスト用、保護者・生徒用

iPhone で Chrome を使っている人や、親に制限を掛けられている人が居て、アプリの入れ方でやや難航。Apple は Chrome から PWA をインストール(ry
各展示の展示員への説明は、講習会に居た展責と担当局員が行ったようです。

入退場口を担当する局員向け講習会の様子
入退場口を担当する局員向け講習会の様子

11/2(11日前): 保護者入場券印刷トラブルの発覚

保護者には教員が印刷した紙の入場券を配布する事になっていて、そこに入場時間帯に合わせた予約 QR コードを印刷してもらう予定でしたが、教員が印刷ミスをしてしまい入場時間帯に関わらず全員同じ QR コードになってしまいました。
再配布してほぼ全てを交換しきる時間は時間は十分に取れないと判断し、保護者チケットについては「2 日間いつでも入れる設定にして、目視で時間帯を確認する」という方針に変更します。

11/8(5日前): 入退場シミュレーション

シミュレーションでは実際に正門でテストを何回か行い、スキャンの速度やミスなどが起きていないかをチェックしました。
スキャンにかかる時間を 1 人 30 秒と想定していたのですが、ぐだぐだだったのにもかかわらず初回の計測から 1 人 30 秒ペースで処理できていたので割と余裕があるとわかり一安心です。

入場処理シミュレーションの様子
入場処理シミュレーションの様子

11/9(4日前): react-qr-reader(というより Worker の GC)の問題発覚

以前から dev server において QR スキャナーを開いていると out of memory でクラッシュする問題が確認されていた[2]ものの、本番環境では一切確認されていなかったため、対応を後回しにしていました。
ところが、筑駒文化祭用のシステム「siesta」の開発メンバー(知人)が公開した『筑駒文化祭入場管理システム "siesta" 開発・運営記』にて react-qr-reader と Chrome の GC についての問題 が報告されており、文化祭 5 日前にしてようやく問題を認知。翌日に件の Issue に追記しました。

問題の概要は「jsQR を動かしている Worker の GC がちゃんと発動されないため、メモリリークが起き、QR コードの読み取りが行われなくなる」というものです。
react-qr-reader の標準では 0.5 秒に 1 回のスキャンですが、CAPPUCCINO ではスキャン高速化のため設定を変更し、可能な限り高頻度(カメラのフレームレートに依存)で読み取りを行っていたため、この問題はとても深刻でした。

結局「unmount して直後に mount することで worker を作り直す」という解決策で Pull Request を立てたのが文化祭当日午前 2 時、コードレビューを経て merge・release されたのが午前 10 時でした。時間がなくて実際に治ったかとうかを確かめられてはいませんが、おそらく治ったと信じています。
9 時入場開始であることとページを切り替える際にもアプリが更新されることを考えるとこの問題に直面した人は少なかったと思いますが、手入力に起因するであろうエラーが散見されたため、そもそも jsQR の読み取り精度の問題でうまく読み取れない問題は発生していたようです。

https://github.com/afes-website/cappuccino-app/pull/237

11/11~12(1, 2日前): 文化祭準備日

(準備日は 2 日前の授業終わりから始まります。)
まずは各展示にテスト用のリストバンドを配布し、各展示員が入退室処理をできるようにしてもらいます。
また、入場口を設営して当日入る局員たちに向けてあらためて説明します。スキャン以外のところで少し問題が起きてるようでしたが、それは私たちの管轄ではないので私たちはただ傍観してました。

4-4. 文化祭当日

入場開始の遅延

文化祭初日、9:00 入場開始予定だったのですが実際に入場が始まったのは 9:12 でした。入場は 30 分単位で時間帯を区切っているので、 9:30 組のスキャンまで遅延しあわや致命傷になるところでした。ここまで遅れてしまった原因は 2 つ。

まずは入場開始の指示が来なかったということ。おそらく開会式をやっている最中で入場をまだはじめたくなかったみたいで、連絡をするまで待ってくれと言われて待機していました。

もう 1 つはポケット Wi-Fi が入口に置いてなかったということ。遅延時間的には高々 2~3 分なのでさほど大きな問題ではなかったですが、入場を始めると宣言して初っ端こうなってしまったので入口は大混乱になり、最終的には自分のスマホでテザリングして乗り切りました。(明らかな確認不足なのですが、そもそも麻布生の集合時刻は 8:00 なのに 9:00 時点で班員が自分(たけ)しかいなかったのがいけない…。)

「え、インフォって統制だけなの!?」~当日連絡が招いた怒涛の紙記録~

リストバンドの紛失対応や入場処理ミスなどは「101 インフォメーション」展示にて行う予定でしたが、紛失や入場ミスの件数はさほど多くないから私たちが直接対応すれば十分だろうとたかをくくっていたために、特に説明をせず当日の朝を迎えます。

リストバンドはアプリスキャンを通してデータベースに登録しないと各展示に回れないシステムにしていたのですが、当日 101 を管轄していた統制局員はそのことを知るよしもなく、何も疑問を持たずに登録していないリストバンドを直接渡して対応していたようです。
リストバンドの再発行は受付を管轄している運営局員が行うと勝手に思っていたので、入場できないお客さんがいるとなれば私たちに必ず連絡するだろうと油断していたのもまた私たちの間違いでした。

結果、リストバンドは暫くの間システムに登録せず ID を紙に記録することで処理されていたために(この流れ siesta の記事でも見たな……)、リストバンドを紛失した来場者や入場処理でミスをされた来場者は、各展示とインフォメーションを何度も往復させられていたようです。この件でご迷惑をおかけした方々へ、本当に申し訳ありませんでした。

登録されていないリストバンドの対応処理についてのマニュアルを慌てて作成し 101 に置いてはもらったものの、2 日間を通して多種多様な問題が起きてしまい、その度に 101 に赴いて登録の仕方を説明することになってしまいました。
特に 2 日目の朝、登録されていない生徒のリストバンドが大量に出回ってしまったときは焦りました。これに関しては生徒のリストバンドをすべてデータベースに登録して対応しました。

一斉退場時は退場スキャンをやめた

一般来場者の時間帯は大きく午前・午後に分かれており、午前の最後・午後の最後はそれぞれすべての来場者が退場します。
その結果、退場スキャンの必要性が薄れ、また退場スキャンのしづらさが問題に上がったことから、結果として 1 日目の午前、2 日目の午前午後の一斉退場時には退場スキャンを行わずそのまま退場してもらい、手作業でデータベースを書き換えて処理することとしました。

この計画外の変更が、翌日に重大インシデントを引き起こします……

すべての生徒を revoke しちゃった事件

前述の通り、一斉退場時の退場スキャンを廃止したので、12 時と 15 時半に手作業ですべての来場者を退場処理する必要性が生じました。

そこで以下のような SQL 文を用意し、手作業で phpMyAdmin[3] から実行していました。1 日目はこれでなんとかうまく回っていました。

UPDATE guests
  SET revoked_at='2021-11-13 12:29:59'
WHERE revoked_at IS NULL
  AND term_id LIKE 'Day1%'

ところが 2 日目の昼休みは別件で忙しく、昼休みの間に退場処理をできなかったために午後の入場が始まってしまい、大慌てで以下の SQL 文を作成し、実行しました。

UPDATE guests
  SET revoked_at='2021-11-14 12:29:59'
WHERE registered_at < '2021-11-14 12:00:00'

これにより、すべての生徒が強制的に退場されてしまったほか、2 日目午前中までのすべての来場者の退場時刻が 2021-11-14 12:29:59 になってしまいました。
いくら焦っていたとはいえ、あまりにひどすぎます。
過ちに気づき即座に生徒の退場を取り消したため大事には至りませんでしたが、展示に入室できないトラブルがいくつか発生していたようです……

実行するべきだった SQL 文は次のようになります。全然条件が足りてませんね……

UPDATE guests
  SET revoked_at='2021-11-14 12:29:59'
WHERE revoked_at IS NULL
  AND term_id LIKE 'Day2%'
  AND registered_at < '2021-11-14 12:00:00'

反省点

  • 焦らず複数人によるチェックを経るべきだった
  • 影響される行を実行前に確認するべきだった
  • 1 から書くのではなく、前日まで使っていた SQL 文を使い回すべきだった
  • そもそもこんな作業を手作業でやるべきではない

始末書っぽくて好き。
このSQLが実行されたとき、僕は文化祭の油そばをすばると2人で食べるためにチケットを買いに行っていて、ふわわあは僕らが寝てても問題ないと言った上で寝ていたためにすばる一人で書くことになりました。
実行するときに僕も確認するべきだったなと反省しております。
あと個人的に、このすばるって人失敗に対してすごく厳しいのでもう少し色々寛容になって欲しいところです。 --- 班長 たけ

リストバンドの予備色の使い方を考えていなかった

1 日目の終わりに「保護者リストバンドが入った箱を紛失してしまい 2 日目の分が足りない」可能性が生じ、予備色として準備していた緑色の使用を検討しました。しかし入場処理時には予約色に対応したリストバンドのみ通す仕様であり、保護者に対応した紫色以外のリストバンドを使用する手段を用意していませんでした。

結果として保護者リストバンドが入った箱を発見したため大事には至りませんでしたが、緑色はすべての予約に紐付けることができる、などといった「予備色の使用手段」を用意するべきでした。

入場口で発生したトラブルのピックアップ

  • 一般枠(中学生以上 1 人)と児童枠(小学生+保護者 1 名)の勘違い
    一般枠で小学生とその保護者が入場しようとしていました
  • 子連れや 2 人で入ろうとする保護者
    保護者は生徒 1 人につき 1 名のみ入場可能としていました
  • 複数人で 1 つの予約(一般枠)を使いまわしていたため、2 人目で入場済みエラー
    同時に来たからよかったものの、別のタイミングだと第三者による不正利用を疑う必要がありました……
  • 時間帯を変えて欲しいという要求
    教師演芸会を見たかったんじゃないかという噂。流石に知らない
  • 予約していない OB が文化祭に入ろうとしていた
    教員と後輩が頑張って追い返していました。また、文化祭終了時刻に麻布生以外を追い出す際、リストバンドの色によって「麻布生なのか OB なのか」を判別していたそうです。

5. あとがき

すばる

ウェブフロントエンドほぼ未経験からここまで 2 年間、コーディングに入り浸って過ごしてきましたが、とにかく楽しい日々でした。
当然ここまでの大きなプロジェクトに参画するのも初めてでしたし、ひとまず完遂できて安心しています。

元は 73 のためのものだったけど、こうして晴れ舞台が見れてとても幸せです!
採用してくれた 74 文実、運用してくれた運営と統制の後輩、ご協力頂いた来場者の皆様、一緒に作ってくれた 2 人、本当にありがとう!!

たけ

文化祭直前期に入ってフロントエンドのお手伝いをしましたが、デザイン以外は楽しかったです。コーディングはまじでふわわあさんにお世話になりました。
また、ご協力頂いた関係各所や保護者・来場者の皆様、本当にありがとうございました。

文化祭が明けたあとも、帰りの電車内などで CAPPUCCINO についてのトークを聞けて結構影響の大きいものを作ったんだなぁと改めて実感します。

ただ、僕らのプロジェクトは文化祭をより良くするためのただのオマケでしかありません。主役は後輩たちですから、僕らのことよりもこのご時世で予約も必要な状況下でお客さんを呼び寄せられて成功させられた後輩自身の事を褒めてほしいですし、麻布生以外の皆様は是非来年の文化祭に(も)ご来場いただけると嬉しいです。

そして、名ばかり班長ながらもこの2年間様々なトラブルや問題を一つ一つ解決しながらここまでやってこれて本当によかったです。ありがとうございました!

ふわわあ

これまで経験していない(どころか経験できるとも思っていなかった)これほどの大きなプロジェクトを最後まで運用できたことを嬉しく思っています。それを許してくれた環境と関係者の方々に感謝。
他メンバーを振り回したりした気もしますが、チーム皆で好きなことに全力を注いで楽しい2年間を過ごせました。本当にありがとうございました。

さいごに

来年はこんなものを使わなくても文化祭を開催できることを願っています。

システム自体の評判がかなり高くて来年も使いたいと言っている人がいるのはとても嬉しいのですが、リストバンド 9000 本の印刷に 16 万円かかっているので、3 万人呼んだときの費用は考えておいてね。

CAPPUCCINO 開発チーム 一同

6. おまけ: データ展覧会

入場者数

2 日間合計で 2574 人が入場しました。

また、最大で 28 人/分 の速度で入場処理していたようです。ラッシュ時は 4 レーンで捌いていたので、1 レーンあたり 7 人/分。すごい。

2 日間の入場者数
2 日間の入場者数

2 日目午後が安定して速くてすごい。すごいしか言葉が出ない。

たくさん人が入った展示ランキング

各展示の延べ入室人数です。

順位 展示名 人数
1 生物部展 1347
2 カラクリ工房展 1268
3 ピタゴラスイッチ展 1101
4 Youは何しに鉄研へ?展 987
5 シン・カガク展 882
6 麻布ミュージアム展 761
7 脱出ゲーム展 749
8 縁日展 741
9 麻布展 706
10 模型同好会展 611
(参考) フロンティア展示 1834

データからわかるフロンティア展示[4]と理科棟展示の圧倒的人気。麻布の文化祭といえば、という感じがすごいです。

麻布生に人気な展示ランキング

麻布生のみの場合です。

順位 展示名 人数
1 カラクリ工房展 431
2 縁日展 411
3 脱出ゲーム展 392
4 生物部展 415
5 麻布ミュージアム展 355
6 Youは何しに鉄研へ?展 344
7 シン・カガク展 337
8 ピタゴラスイッチ展 332
9 カジノ展 332
10 アザ娘フィジカルダービー展 330
(参考) フロンティア展示 850

平均展示滞在時間ランキング

展示の入室から退室までの時間の平均値です。

順位 展示名 時間 (分)
1 ボードゲーム展 40
2 カジノ展 38
3 しょーーーーーーぎぶ展 27
4 生物部展 大実験室 23
5 Chess展 23
6 Youは何しに鉄研へ?展 22
7 討論部展 22
8 音屋展 22
9 囲碁展 20
10 電脳展 19

そのほか

  • 用意した一般向けの予約 1,196 件に対し、実際に来場した予約は 946 件で、来場率は 79.1% でした。
  • データベースに登録された行動履歴 (ActivityLog) の件数は 50,398 件でした。1 人あたり平均 12.3 件記録されています。
  • 展示で「定員に達しました」と表示された回数は 249 回でした。
  • 一番最後の入室記録は 2021-11-14 21:21:24 で「ボードゲーム展」への入室でした。
  • 文化祭翌日以降に「展示に入室しようと試みた」回数は 15 回でした。文化祭 5 日後になっても入室を試みる人がいました。
脚注
  1. 便宜上こう書きましたが、正確には今年度の文実には所属しておらず、文実外から後輩に協力するという形で参加していました ↩︎

  2. 文化祭 1 ヶ月前にあたる 10/13 の時点で Issue #178 · afes-website/cappuccino-app が立っています ↩︎

  3. ConoHa WING のデータベースは外部からの接続ができません…… ↩︎

  4. フロンティア展示:ミニ展示が 1 教室に最大 9 つ入った、麻布生の思い思いのミニ発表の場。また、1 教室内で行うフロンティアステージもあります。今年は 46 展示が参加しました。 ↩︎

GitHubで編集を提案

Discussion

diskaitodiskaito

こんにちは。プラグラミングも初心者な高校生3年生です。今回の文化祭で、cappuccinoと同じようなプログラムを制作したいと考えています。何もわからないので少し詳しいプログラムの内容をお聞きしたいのですが、お願い出来ないでしょうか。

すばるすばる

そう言ってくださるのはとても嬉しいのですが、現実としてはかなり難しいのではないかと思います。ある程度の経験がある 3 人で 1 年以上掛けて制作しており、前提知識もかなり要求されます。

本文中にも追記しましたが、使用したコードはすべて GitHub にて管理しており、詳しい内容についてはそちらをご覧いただければと思います。

diskaitodiskaito

返信ありがとうございます。
想像はしていましたが、やはり難しいのですね。
わかりました。代替案を考えてみます。