🧑‍💻

東海高校 “ハイテク”記念祭を支えたシステム

2023/04/27に公開

東海高校生徒会 創立134周年記念祭実行委員会 予約システム担当(?)[1]のktkです。昨年度、9/24-25で東海高校において創立134周年記念祭が催行されました。3年ぶりの実地での記念祭催行に伴い、制作したシステム等の紹介を行います。

今年度も...

昨年度の機関誌で、「ウェブページの裏側」と題して、オンラインでの記念祭を実現した技術について解説を行いました。解説記事をお読みいただいた方、また、興味を持って質問に来られた方、大変に励みになっています。本当にありがとうございます。
あれで終わりだと思っていたのですが、高3にもなって何故か徴用されました。タスケテ...

概要

何したの?

今年度の記念祭では一般来場者の人数制限のため抽選を行い、当選者に予約サイトで各個人に割り当てられたQRコードを表示できるようにし、それを用いて入場改札を行いました。また、中高全生徒にも同様のQRコードを配布し、各クラスでの観覧者の全数把握を行いました。

(多分)東海だけっ!

自分が関与しているオンライン開催の132周年や133周年、大先輩方が作成された実地開催時の過去のウェブサイトと同様に、今年度もまた全てのシステム・アプリケーション等は記念祭実行委員会のオリジナルで、外部の業者への委託等を一切していません。また、実装に教職員が関わることなく全て生徒のみで作成しています。
他の高校や大学などで開催されている文化祭では、家族限定であったり、在校生からの招待限定であったり、当日の人数制限で入場可否が決定したり、あるいは三菱総研のmiraicompassを利用して申し込みをとる学校などもありました。
愛知県や文科省から示されているイベント開催時の必要な感染防止策等のうち、特に来場者情報の把握・管理手法の確立及び参加者の連絡先把握を行っていて、一般来場者を受け入れ数千人規模で文化祭を催行できた名古屋近辺の高校は、調べた限りでは東海のみです。

システム全体像

応募・抽選時

一般来場者

  1. 一般来場者は、実行委員会側の予約システムで登録をし希望する時間帯を選択して応募を行います。
    応募者のアカウントで複数人のグループを作成し、グループ全員で一括に申し込むと行ったこともできます。
  2. 実行委員会が抽選を行い、その結果をメールで応募者に通知します。
  3. 当落結果の発表時点から当選者にQRコードが予約システム上で表示できるようになります。

保護者

  1. 学校側が作成したGoogle フォームへ応募を行い、教職が抽選を実施した上でその結果がエクセルファイルで実行委員会側へと渡されます。
  2. 実行委員会でそのファイルを元に実行委員会側の予約システムのアカウントを作成し、それと対になる入場用QRコードのデータを渡されたエクセルファイルに付与し返却します。
  3. 教職が全校生徒を通じて、保護者へQRコードを紙で配布します。または、Google フォームへの応募時に入力されたメールアドレスと生徒の情報を基に、予約システムにログインしてQRコードを表示します。

高校生徒

実行委員会側の予約システムで、学校から配布されているGoogleアカウントを使用しログインし、予約システムでQRコードを表示します。

中学生徒

記念祭当日の朝礼時に全生徒にQRコードを紙で配付しました。

入場改札時

一般来場者と保護者

正門付近に設置された改札でQRコードを提示し、それをiPadで改札システム使い読取り、入場します。

再発行対応

スマートフォンを持っていない・アカウントのログイン情報が不明といった生徒のために、各階EVホール前に再発行所を設け、再発行対応を行いました。
また、QRコードを家に忘れた・表示ができないといった一般来場者と保護者のために、改札付近の入場管制に再発行端末を設置し、再発行対応を行いました。

高校1階EVホール前再発行

高校クラス企画入場時

各クラスへの入場時に入場者のQRコードを読取り、クラス内へ入場した人物の全数記録を行いました。また、入場制限人数に達した状態でQRコードを読取るとエラー音を発するようにし、入場が不可能であることを、目と耳でわかるようにしました。

QRコード仕様

QRコードはオフラインでも入場記録ができるよう、所有者の情報を含むことが求められます。ただ、QRコードの内部構造を読み取られてしまい、偽装QRコードが簡単に作成できると万が一改札システムがオフラインでの運用となった際に改札を突破できてしまうため、これは避ける必要がありました。しかし、暗号化を複雑にすれば解決かというと、あまりに細かすぎると紙で印刷されたQRコードを高速に読み取れなくなります。
そのため、

  • 内部UUID
  • 氏名
  • 個人識別データ(保護者/生徒であれば学年クラス番号等、一般来場者であれば年齢性別等)

をMsgPackでエンコードし、さらにAES/CBC/PKCS5Paddingでほどほどに暗号化したものをQRコード内の文字列として使用しています。
また、読み取り精度向上のため、改札で紙のQRコードを提示した来場者に対してクリアファイルを配布する措置をとりました。

各システムの詳細

予約システム

予約システムは、来場者のグループ作成や応募申し込み、当選後のQRコード表示などの機能があります。高校保護者向けのクラス企画事前予約を承るシステムや講堂・ステージのタイムテーブルの表示ページもこのシステムです。ウェブページにはNuxt2 + Vuetify2をSSGでVercelで動かし、システムへのログイン機構は、昨年度に催行したオンライン文化祭での学内公開時に利用したFirebase Authenticationとほぼ同様のGoogle Cloud Identity Platformで実装しています。まず始めにログイン機構から解説します。

ログイン機構

一般来場者や高校生徒のログイン処理は通常のGoogle Cloud Identity Platformと同様ですが、中高保護者は生徒会顧問団が事前に希望を集め抽選が行われた結果を基にアカウントを作成しているため、とても複雑な処理となりました。内部でのアカウント種別は、一般来場者、中学生徒、高校生徒、中学保護者、高校保護者となっています。

まず、中高保護者についてです。
生徒会顧問団がGoogleフォームで事前に希望を集めて抽選を行った関係で、例えば兄弟で在籍している生徒の保護者が両方当選し、同じメールアドレスで二人の当選者が出ると行った事象が存在しました。しかし、ログインプロバイダの仕様で1つのメールアドレスに対して1つのアカウントの作成しか行えません。そのため、ログインプロバイダ側のアカウントにDBで管理しているアカウントを複数対照させ、それらをログイン時に選択させる方式をとりました。具体的には、サーバー側に対する全てのリクエストに選択したアカウントのインデックスパラメータを付与し、それに基づいてバックエンドから返却するデータを変更する形を取りました。なお、この点に関しては、ログインプロバイダをGoogle Cloud Identity PlatformではなくAzure Active Directory SSOを使用することで回避できた可能性があります。 

ログイン後、一部の保護者にさらに表示されるアカウント選択画面

次に、中高生徒についてです。
弊学では、中学校では各保護者に対して、高校では各生徒に対してGoogle Workspace for Educationのアカウントが配布されています。また、中学生徒は学校内へのスマートフォンの持ち込みが原則禁止されています。
このような事情から、中学生徒に対しては紙でQRコードを配布し、高校生徒は各々が予約システムでQRコードを表示させるという形態を取りました。学校から配布されているアカウントが存在することで、生徒であることの認証が非常に簡単に行えるため、高校生徒のログイン機構の実装は保護者に比べると非常に簡単でした。
配布されているアカウントはドメインがtokai-jh.ed.jpでしたので、高校生用のログイン画面ではドメインを限定してログインすると言った実装を行いました。以下は、@nuxtjs/firebaseを使用して、ログインができるアカウントのメールアドレスのドメインを、制限してリダイレクトログインページへ飛ばす実装です。例として弊学のtokai-jh.ed.jpとしています。これはクライアント側での処理となるため、このほかにもMiddlewareでClaimを読んで弾く等のの処理も別途必要です。

const provider = new this.$fireModule.default.auth.GoogleAuthProvider().setCustomParameters({hd: 'tokai-jh.ed.jp'});
this.$fire.auth.signInWithRedirect(provider)


ドメインを制限してGoogleアカウントでログイン

保護者及び学生のアカウントは学校側からのメールアドレス情報などでログインプロバイダに予め登録処理を済ましてあり、保護者の場合は生徒の情報とメールアドレスの認証、生徒の場合はGoogleアカウントのみで、簡単にログイン処理を行うことができるようにしました。

保護者ログイン

生徒ログイン

最後に、一般来場者についてです。
一般来場者は、予め登録処理が済んでいる前記のものと違い、来場者自身が登録を行う必要があります。登録時のメール認証はすべてログインプロバイダに丸投げができるので、メールサーバーの構築などは不要です。ただ、送信される文章の変更が効かず日本語としては若干自然な部分もあるので、自前で送信する方が良かったのかもしれません。
以下は、@nuxtjs/firebaseを使用して、アカウント作成と共に認証メールの送信を行う際の実装です。

this.$fire.auth.createUserWithEmailAndPassword(/*email*/, /*password*/).then(res => {
        res.user.sendEmailVerification({url: window.location.origin + 'メール認証後のページ遷移先のパス'}).then(() => {
          res.user.getIdToken(true).then(idToken => {
            localStorage.setItem('access_token', idToken.toString())
            localStorage.setItem('refresh_token', res.user.refreshToken.toString())
          })
        }).catch(err => {
		  //省略 sendEmailVerificationのJSDocにエラー一覧の記載があります。
          console.log(err)
        }).finally(() => {
          this.$router.go({path: "メール送信後のページ遷移先のパス", force: true})
        })
      }).catch(err => {
        //省略 createUserWithEmailAndPasswordのJSDocにエラー一覧の記載があります。
        console.log(err)
      })

Google Cloud Identity Platform/Firebase AuthenticationではCustom Claimsという機能があり、Firebase Admin SDKからアカウントに対して任意の情報を付与することができます。記念祭では、各アカウント種別に応じたクレームや、当落情報や時間帯情報のクレームを付与したりしていました。この機能を使うことで、Middlewareでのページ遷移をより簡単に実装することができました。以下のような形で、Vuex Storeでの処理を行いました。Custom Claimsは、Firebase Admin SDK以外から、つまりはユーザ権限での変更ができないので、Middlewareでも値を信用することができ、安心して処理を行うことができます。

async onAuthStateChangedAction({commit, dispatch}, {authUser, claims}) {
    if (!authUser) {
      commit('RESET_USER')

      return
    }

    const {uid, email, emailVerified, displayName} = authUser

    commit('SET_USER', {
      uid,
      email,
      emailVerified,
      displayName,
      registered: claims.registered,
      parent: claims.parent,
      tokaiJH: emailVerified && authUser.email.endsWith("@tokai-jh.ed.jp"),
      school: claims.school,
	  //一部省略
    })
  },

来場応募機構

記念祭への一般来場応募を取り扱う部分です。
土曜/日曜及び午前/午後の4つの時間帯から複数選択可能となっています。応募者数が想定よりかなり多かったため発生はしませんでしたが、複数時間帯の当選も可能な仕組みです。土曜午前と土曜午後の申し込みの場合は2^0+2^1=3、といったような形で内部での予約時間の管理はビット演算で行いました。

応募ページ

抽選に関しては各方面から様々な注文があり、かなり頭を抱えましたが、全てを無視し完全ランダムで実施しました。手法は応募者からランダムに1名抽出し、その応募者が希望している時間帯をランダムに並び替え、一つずつ取り出したものに空きがあれば、当選させるといったものです。あとは、ここにグループの処理が加わってくるのみで、つまるところの確率操作に比べて格段に楽な実装ができました。

知り合いが落選して感情を露わにする記念祭幹部(高校2年生)たち

当落通知は、Azure MarketplaceのSendGridを用いてメール送信を行いました。数千件のメール通知を一斉に送ったので、途中で送信が規制されましたが、サポートチケットを開いて事情を説明すると、数時間で規制は解除され、結局6時間程度で全メールの配信を終えることができました。
後の祭りですが、グループ機能に他の登録者を招待する機能を実装していたので、招待時にWeb上の表示のみで通知を行っていたものを、実際にメールを送信していれば、メールアドレスのウォームアップにもなって送信が規制されることもなくなって良かったのかなぁと個人的に思っています。

落選メール文面

高校クラ企予約機構

高校保護者のみが使用可能な、高校クラス企画の事前応募を取り扱う部分です。
こちらも応募機構と同様にビット演算を使用し、ランダムな抽選を実施しました。
クラスによってバラバラに存在する公演を行わない時間枠や、自クラス優先保護者枠であったりを考慮して、高校クラス企画事前予約枠の設定を行う必要があり、エクセルで表を作り、CSVに出力し、KotlinでJsonに整形したりSQLに流し込むといったことを行いました。

割当催行時間表

バックエンド

バックエンドには、昨年度の動画提出ページでの実績があるAzure Functionsを採用しました。クライアント側から送信するリクエストは、ログイン状態の場合にはAuthorization Headerへへトークンを含ませ、バックエンド側でFirebase Admin SDKを使用して認証を行い、リクエストの処理を行いました。
Firebase Admin SDKはライブラリがJSとJavaに存在したため、フロントエンド側はもちろんのこと、Kotlinで実装したバックエンド側のAzure Functionsでも楽に実装ができました。

クライアント側

await this.$axios.$post(/*endpoint*/, /*data*/, {
          headers: {
            'Authorization': `Bearer ${await this.$fire.auth.currentUser.getIdToken()}`,
          }
        }).catch(err => {
          console.log(err)
        })

サーバー側(Kotlin)

@FunctionName("example")
fun example(
    @HttpTrigger(name = "req", methods = [HttpMethod.GET], authLevel = AuthorizationLevel.ANONYMOUS) request: HttpRequestMessage<Optional<String?>>,
    context: ExecutionContext
): HttpResponseMessage {
    request.headers["authorization"]?.let { authorizationHeader ->
        val token = authorizationHeader.split(" ")[1]
    	val decodedToken = FirebaseAuth.getInstance(/*Firebase App インスタンス*/).verifyIdToken(token)
	//省略
        return request.createResponseBuilder(HttpStatus.OK).build()
    }
    return request.createResponseBuilder(HttpStatus.BAD_REQUEST).build()
}

データベース

データベースは以下のような構成です。中学生徒やメールアドレスを保持していない保護者、単一メールアドレスで複数当選している保護者が存在していたため、各ユーザにGoogle Cloud Identify PlatformのUIDとは別に内部用のUUIDを割り当てました。
なお、取得した個人情報(名前, メールアドレス, 電話番号, グループ名)は、10/31に生徒会顧問団長と指導部のIT担当の教職の確認のもと削除し、全てのデータに対して匿名化処理を施しました。

  • ログイン情報テーブル(Google Cloud Identity Platform ユーザuid, 内部UUID)
  • ユーザ情報テーブル(内部UUID, 名前, アカウント種別)
  • 電話番号テーブル(内部UUID, 電話番号)
  • 生徒情報テーブル(内部UUID, 中/高, 学年, クラス, 番号, 名前)
  • 生徒保護者対照テーブル(内部UUID, 中/高, 学年, クラス, 番号)
    その他当選情報やグループ情報、保護者枠制限人数など

改札システム

改札システムは、正門付近に設置された改札でiPadを用いてQRコードを読み取り入場を確認するものです。
高速なAPI処理で、来場者の数が多いときにも目視での確認と比較して、スムーズに入場できるようにしました。オンライン時は、Azure Functionsに入場情報を確認しますが、万が一のネットワーク不通のために、オフライン時にはLocalForageに入場者を保存し、インターネットに接続したときにAzure Functionsへ順次送信する機能も備えました。
改札では、実行委員がiPadを持って立ち、QRコードの読み取りを行いました。改札の通過速度は想定よりもかなり速く、列が発生することはほとんどありませんでした。

ジャンク品ネットワーク

同一QRコードでの複数回の入場を防ぐため、改札に関してはインターネット環境が絶対の条件となりました。雨天時は改札が高校来客玄関前で展開される予定で、205補助教室からLANケーブルを伸ばすことができたり、学校側のWiFiがギリギリ届いていたりしていたため、特に問題はありません。
しかし、晴天時の改札展開場所である同窓会玄関前、いわゆる椎尾アイランドの付近にはインターネット環境がありません。そのため、大須で30m500円のLANケーブルを2本調達し、転がっていた適当なスイッチに繋いで高校生徒会室からLANケーブルを60m這わせ、高校放送室に落ちていたWiFiアクセスポイントを椎尾アイランドの時計の下辺りに設置してインターネット環境を確保していました。
総工費1000円。

入場記録システム

入場記録システムは、高校各クラスに貸出しのiPadで、クラスの入場管理担当者が、企画を観覧するためにクラスに立ち入った全ての入場者を記録するためのものです。
実行委員ではない生徒が扱う端末なので、話を聞いていない・説明文書は読んでいないという前提で、全ての入場はアプリでのQRコードの読み取り結果に任せるということを目標に開発を行いました。そのため、高校保護者の予約データや中学1年生の予約データ、自クラス優先保護者枠のデータも事前にローカルストレージ内に事前に保存し、読み取ったQRコード内のデータを参照して、入場可否の判断をすべてローカルかつオフラインで完結するようにしました。

設計時には校舎内にWiFiが設置されておらず、オフラインでの入場記録が必須であり、そのためにはタブレット端末用にネイティブアプリを作成して行うのが最も簡単な方法であろうと考えました。特にiPadの貸出費用はAndroidタブレット端末のそれと比較するとかなり低く抑えられるため、計画当初は中学クラス企画やクラブ有志企画も含めて100台近くを配備する予定でしたので、予算的な面からiPadが優位な選択肢でiPadを選択しました。
しかし、所有しているノートPCはThinkPadでXCode環境が存在せず、また、Appleのアプリの審査体制やそのベンダーロックインとも取れる姿勢に、私は嫌悪感を持っています。そのため、Nuxt PWA + LocalForageを使用し、あくまでウェブアプリの範疇でネイティブアプリのような挙動をするような実装をしました。
このような設計ですが、実装の不備などを考え、その後設置された学校側のWiFiに専用のSSID一つ作っていただき、念のためにインターネットへ接続をしていました。
これが後に功を奏し、古い記念祭要項に基づいて実装したままその後の変更への追従を忘れていてクラス入場開始を5分遅く設定していた不具合が発覚した際に、ページリロードのみで修正版への更新が行えました。

マンパワー

改札13台と高校1・2年の計20クラス分の合計33台のiPadに、完成したPWAアプリをインストールする作業は、Apple Configurator 2を用いて一括で行いました。ただ、ウェブアプリのボタンを押すといったことはできなかったため、改札やクラス用にデータをダウンロードするなどのアプリの初期設定はiPadを横に並べてどんどん押すといった形でマンパワーに頼る形となりました。

改札と入場記録の、オフライン状態でもローカルで処理を完結できるようにするという実装は、Suicaの改札機[2]から着想を得ました。

入場用QRコード再発行端末

正門の改札付近の入場管制と高校各階に展開していました。たまたま数ヶ月前にSUNMI V1sが話題になっていたときにおもしろ半分で個人で買ってみたところ中々に使えたので、個人持ちの1台を入場管制に、実行委員会でも3台購入して高校各階に配置しました。

こちらは他のアプリとは違い、KotlinでJetPack Composeを用いたAndroidのネイティブアプリを使用しています。SUNMI方言のESC/POSは中々に難しく、一般的なプリントライブラリでは対応していないため、ドキュメントやSUNMI公式のデモを参考に、文字装飾は制御用コードを一つずつ送りつけることになるなど実装に大変苦労しました。

QRコードをプリントしたレシートとSUNMI V1s

開発タイムライン

  • 7/20
    様々な都合から予約や整理券をQRコードで管理するシステムの開発が決定
  • 8/6
    実装開始
    デジタル庁の接種証明書アプリを参考に生徒のQRコード表示の基本デザイン作成

    プロトタイプ
  • 8/10
    中学生徒一覧・高校生徒アカウント一覧を拝受
  • 8/16
    生徒/保護者のログイン後QRコード表示実装
  • 8/22
    一般来場者の登録実装
  • 8/23
    中高保護者の抽選結果一覧を拝受
  • 8/26
    一般来場者の来場予約応募実装
  • 8/27
    グループ機能実装
    中高保護者の抽選結果一覧にQRコードのデータを加えて返却
  • 9/2
    予約システム稼働
  • 9/3
    一般来場者の来場予約応募の受付開始
  • 9/9
    高校保護者の高校クラス企画事前予約実装
  • 9/11
    高校保護者の高校クラス企画事前予約(抽選)受付開始
  • 9/17
    一般来場者の来場予約応募の締切
  • 9/18
    高校保護者の高校クラス企画事前予約(抽選)締切
    一般来場者の来場予約の当落発表
  • 9/19
    再発行用端末のアプリ実装
  • 9/21
    高校保護者の高校クラス企画事前予約(残枠先着)受付開始
  • 9/23
    高校保護者の高校クラス企画事前予約(残枠先着)締切
    改札システム・入場記録システム実装
    iPadへウェブアプリ注入
  • 9/24・9/25
    当日

    完成品(高校保護者の表示)

前夜祭配信

記念祭前々日の木曜に開催された前夜祭の配信について紹介します。

前夜祭の現地での観覧人数を制限する関係上、その代替手段の確保というものが必要です。132周年はYouTube、その後のGoogleのポリシー変更によりYouTubeの使用ができなくなったため133周年は記念祭ホームページ上(Azure Media Service)で行いました。今年度は実地開催であると言うこととで、高校全クラスへのテレビ放送によって代替手段を確保しました。職員駐車場付近から明照殿まではHDMIケーブルで、明照殿からは明照殿―(旧)中学放送室間及び(旧)中学放送室―高校放送室間に敷設されていた既設の映像線・音声線を利用し、自主放送設備のある高校放送室まで伝送を行い放送しました。校内の既設回線は、映像が75Ωの同軸(BNC)でアナログコンポジット信号、音声がアナログバランス信号です。
放送中に高校各教室を回ったところ、クラス企画準備がなくテレビが物理的に隠れていない高校3年生で特に多くの生徒に視聴されており一定の効果はあったものと思われます。

総括

私はWebに関して専門外で、デザイン面であったりCORSやSSOのトークン更新の仕様であったりなどなど、様々に実装に苦労した箇所もありましたが、システムがダウンすることなく催行を行えたので、自分としては成功したと言っても良いのではないかと思っています。特に個人開発では、ここまでクラウドや各種端末を贅沢に用いたシステムを作成することは難しく、大変に良い経験を積むことができました。

さいごに

このシステムで行くということに承諾をくださり、各方面との調整に奔走してくださった生徒会顧問団長を始め、ご協力頂いた教務・各指導部や教職員方、当日改札運用担当の実行委員や再発行ブースを担当いただいた132のOBの皆さまには頭が上がりません。この場を借りて感謝申し上げます。
本当にありがとうございました。拝

追伸: こんなこと1人にやらせんな。もう二度とやる♡

脚注
  1. 高校3年生なので実行委員ではないけど幹部で担当者という微妙な立ち位置 ↩︎

  2. 椎橋章夫,ICカード乗車券システムにおける自律分散高速処理技術と
    そのアプリケーション https://www.sice.jp/ia-j/papers/jitk6-20050722-1305.pdf
    椎橋章夫, 自律分散型 IC 乗車券システム “Suica”の開発と導入 -交通インフラから社会インフラへの進化- https://www.jstage.jst.go.jp/article/bplus/14/1/14_60/_pdf ↩︎

Discussion