🫓

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

2023/07/07に公開

久々にブログ記事を書くような気がします、地方のとある私立高校に通う高校2年生です。

今回は色々と縁があって、学校の文化祭で使用する文化祭入退場管理システム"Cracker"を1人で開発したのでその開発・運用記を書いておきたいと思います。今後同じような事をする人に少しでも役に立てればと思います。
(もはや界隈では入退場システムはよくあるやつで、最近は食券販売や会計システムも作っていますね。楽しそう)

ちなみにこの記事のアイキャッチはフラットブレッド(なんだそれ)。

謝辞
Cracker及びこの記事の構成等は以下の記事を大いに参考にして開発・執筆しました。ありがとうございます。
https://zenn.dev/su8ru/articles/cappuccino-system
https://zenn.dev/newt_st21/articles/gateway-stay-status-record-system
https://qiita.com/ret2home/items/9806d073122dd5e7e873

システム概要

開発・導入目的

今年度、文化祭には保護者だけが入場できるようにする形式をとるようです。そのため保護者だけが入場できるように予約システムを作り、受付で入場処理を行い、入場者を管理するというのがCrackerの目的です。

また他の文化祭入退場システムの記事を見ると、展示会場(教室とか)ごとに入退場の記録をつけるようですが、今回は学校に入場する際だけに使用するので仕様は若干異なるかと思います。

運用の流れ

文化祭事前参加申し込み

文化祭に参加希望の保護者(以下「ゲスト」)には事前に指定のGoogleフォームに氏名や人数、生徒情報、連絡先メールアドレス等を打ち込み、送信してもらいます。
そして入場時に使用するQRコードが添付されたメールが自動的に届きます。

送信される確認メール

入場スキャン

ゲストには事前にメールされたQRコードを入場時に提示してもらいます。そこで入場担当の係員がCrackerを使用して入場処理(データベースにカキカキ)を行います。

ネーミング

参考にした記事がCAPPUCCINOだったり、siestaだったりしたのでスイーツっぽい名前にしようと思いまして、「スイーツ 名前」と調べてCrackerがサービス名でも有り得そうだったのでCrackerにしました。特に深い意味はありません。
あとから調べると、

クラッカー (Cracker) は、コンピュータネットワークに不正に侵入したり、破壊・改竄などの悪意を持った行為、すなわちクラッキング(悪意を持ったハッキング)を行う者のこと。最近はセキュリティ・ハッカーと呼ばれることが多い。(Wikipediaより)

とすこし恐ろしめの意味がありました。言われてみると聞いたことあるような。

使用技術

後ろの開発・運営記のセクションでも書きますが、開発猶予は1.5ヶ月という短い期間であったため新たな技術を使うことは断念し、使用・開発経験のある言語をメインに選定するようにしました。

フロントエンド

基本PHPにHTMLベタ書きです。はい。React? Vue.js? Next.js? なんですかそれは?


Crackerトップページ

CSS

CSSに関してはscssを使って、ボタンやモーダルの一部のデザインにCSSフレームワークのBulmaを使用しました。
今までCSSフレームワークを使ったことが無かったですが、ここだけはCSSを書く量を極力減らすため良さげ(直感)なフレームワークを選んで使いました。

QRコード読み取り

あとQRコードの読み取りにはjsQRを利用しました。他の記事を見ると不具合が多少あったっぽいですが、そのようなことは発現せず乗り切れました。

バックエンド

フロントエンドの方でフライングしてしまいましたが、PHPを選定しました。PHPなら利用経験があるのでスピード開発も可能です。
PHPは素のまま、データベースはMySQL、サーバーはclorfulboxを利用しました。
colorfulboxは30日間無料!神!これで学校側の出費が抑えられました!

データベースをしっかり使った開発経験が無かったのでアクセス数によるエラーなど色々心配でしたがどうにかなりました。

ここからはデータベースを指す場合、DB(テーブル名)と表記していきます。
ex) DB(user) : データベースのuserテーブル

データベース構成

第一正規化もされていない...

  • user
key type description
user_id VARCHAR(20) primary key
name VARCHAR(40)
authority int 管理者:1111, 一般:0000
exhibit_id VARCHAR(20)
password VARCHAR(256) ハッシュ化(SHA256)、saltつける
  • guest
key type description
guest_id VARCHAR(128) primary key, uuid
guests longtext [["続柄","氏名"]]
student_info longtext ["生徒氏名","生徒所属","生徒学年","生徒クラス"]
state VARCHAR(20) not_entered/entered/deleted
mail VARCHAR(400)
  • activity
key type description
activity_id VARCHAR(128) primary key, uuid
guest_id VARCHAR(128)
user_id VARCHAR(20)
exhibit_id VARCHAR(20)
activity_type VARCHAR(5) enter/exit
timestamp timestamp
  • exhibit
key type description
exhibit_id VARCHAR(20) primary key
exhibit_name VARCHAR(60)

今回は一つのPHPのプロジェクトにフロントエンドもバックエンドも詰めてしまいましたが、データベースの操作などのバックエンドとなる部分はmoduleとして機能ごとに分けました。
後から機能を拡張していく時に、同じような機能だけど少し仕様が違うものが出てきて命名が適当になっています。RESTfulにしたかったのですが、知識が足りない。

module部分構成
│
├─module : 共通部分(バックエンド機能)
│  │  login_state.php : ユーザーのログイン状態確認
│  │  pass_hash.php : 文字列のハッシュ化(SHA-256/salt)
│  │  sql_config.php : SQLの接続設定
│  │  uuid.php : UUID生成
│  │
│  ├─activity : activityテーブル操作
│  │      activity_get.php : アクティビティ情報の取得
│  │      activity_register.php : 新規アクティビティ登録
│  │
│  ├─exhibit : exhibitテーブル操作
│  │      exhibit_all_get.php : 全展示情報取得
│  │      exhibit_check.php : 展示情報存在確認
│  │      exhibit_get.php : 展示情報取得
│  │      exhibit_register.php : 新規展示情報登録
│  │
│  ├─guest : guestテーブル操作
│  │      entered_guest_get.php : 全入場済みゲスト情報取得
│  │      guest_check.php : ゲスト情報存在確認
│  │      guest_delete.php : ゲスト情報完全削除
│  │      guest_edit.php : ゲスト情報(参加者)変更
│  │      guest_get.php : ゲスト情報取得(ゲストIDから)
│  │      guest_get_details.php : ゲスト情報取得(ゲストID/メールアドレス/ゲスト名から)
│  │      guest_register.php : 新規ゲスト情報登録
│  │      guest_update.php : ゲスト入退場状態編集
│  │
│  └─user : userテーブル操作
│          user_check.php : ユーザー存在確認
│          user_get.php : ユーザー情報取得
│          user_register.php : 新規ユーザー登録
│

参加申し込みフォーム

文化祭参加申し込みフォームは安定のGoogleフォームにスプシ&GASを組み合わせました。GASを用いてゲストIDの作成とメールの送信、データベースへの情報の格納まで行わせました。

システム設計・機能と詳細

参加申し込み

再三言ってしまいましたが、参加申込にはGoogleフォームを利用します。また申し込みはひと家族あたり1回にしてもらい、来場の際は家族で一緒に来てもらうようにしました。
入力してもらう内容は以下の通り。

  • 生徒氏名(兄弟で通っている場合は年上の方)
  • 生徒所属(学年・クラスなど)
  • 連絡先メールアドレス
  • 駐車券の必要有無(必要な人には駐車券が発券されるため)
  • 文化祭参加人数
  • 参加者の氏名と生徒との続柄(人数分)

参加人数によって入力する来場者の名前の数が異なるので、参加人数により氏名の入力欄を変更するようにしました。
https://www.ec-create.jp/google-forms/how-to-use/branch-by-answer/

入力された内容はスプレッドシートに情報が入力されていき、スプシに紐付けられたGASでゲストID(uuid)が生成され、APIを通じてDB(guest)へ情報が登録されます。
また同時にQRコード生成APIを用いてゲストIDが含まれたQRコードを作成します。
そして生成されたQRコードと入力された内容を書き込んだメールを申し込み確認として送信します。

申し込みの期限が過ぎたところで各担任が生徒名簿(?)と登録された情報を照らし合わせ、学校の生徒とその保護者かどうかを確認します。もし生徒名簿に存在しない生徒などで登録がされていた場合、該当のゲスト情報を無効化するようにしました。

QRコード生成API

ただのQRコードだけをメールで送信しても良かったのですが、味気が無いって言うのとQRコードが提示された際に文化祭のQRコードかを目視で一次確認を出来るという理由でQRコード生成APIを作成しました。
QRコード生成APIではQRコードと共にCrackerのロゴなどを含んだ画像を生成します。PHPのGDを使用して作りました。
元のQRコードの生成はQR code APIを利用しました。

生成されるQRコード画像

入場スキャン/処理

入場スキャンはまずQRコードをCrackerで読み取ります。QRコードにはゲストID情報が含まれているので、ゲストIDがDB(guest)に存在するかを確認します。存在すればゲストが入場しているか否かを確認するためDB(guest)のstateを参照し、値がnot_enteredならば入場処理ボタンを表示させます。それ以外の場合各種エラーを表示しておきます。
またゲストIDをコピペして入場処理も出来るように手入力用のフォームも付けました。

スキャンページ

正常なQRコードをスキャンした時

エラーたち

入場処理ボタンが押されると以下の処理がバックエンドで行われます。

  1. DB(guest)のstate書き換え
    statenot_enteredからenteredに書き換えます。
  2. DB(activity)に新規データ追加
    DB(activity)には入場処理や退場処理が行われる毎に、どのゲストが何をしたかを以下の情報と共に格納します。
  • アクティビティID(uuidを生成)
  • 対象ゲストID
  • ユーザーID(誰が処理を行ったのか(担当者の確認))
  • 展示ID(どこに入場したか)
  • アクティビティタイプ(入場なのか、退場なのかを識別)
  • 時間

退場スキャン/処理

退場処理は入場処理と実質的には変わらないので割愛。また当日はこの機能は使用しませんでした。

ゲスト情報確認

ゲストのQRコードを読み取り、ゲスト情報を表示します。
DB(guest)から氏名や状態等の情報を、DB(activity)から今までの行動記録を取得しています。

QRコードスキャン後の情報表示

ゲスト情報検索

DBに登録されているゲスト情報を検索することができます。登録されているゲストIDまたはメールアドレスから検索することができます(完全一致)。
検索をかけると以下の情報が登録されます。

  • ゲストID
  • ゲスト名(代表者のみ)
  • 状態(入場済みor未入場or無効化済み)
  • メールアドレス

反省のセクションにも書きますが、ゲストIDとメールアドレスだけでしか検索できないのは不便であまり利用価値がありませんでした。しかも完全一致となると余計に。
ゲスト名でも検索できるようにするのと、LIKE句等で部分一致検索等を実装しておくべきでした。

検索結果

QRコードスキャン

入退場処理やゲスト情報確認で使われるQRコードスキャンについてもう少し詳しく書いておきたいと思います。QRコードスキャンはJavaScriptとQRコード読み取り用ライブラリであるjsQRを使用して実装しました。

結局使わなかったのですが、書画カメラを使うかもしれないという案が出ていたのでカメラの切り替え機能もつけました。地味に一番苦戦した部分でもあります。
navigator.mediaDevicesを使って接続されているカメラのdeviceIdを取得してlocalStorageに配列で保存しておいて、カメラ切り替えボタンが押されたら今使用中のdeviceIdを取得し、localStorageに保存してある次のdeviceIdを使ってカメラを切り替えるという事を行っています。初回アクセス時はフロントカメラを使います。

スキャンページ

QRコード内の情報が取得できたらGETパラメータに取得したゲストIDの情報とキャッシュ対策用のランダムな数字をセットして画面遷移させるようにしました。
入退場処理を行った後すぐにもう一度スキャンをするとキャッシュのせいでゲストの状態(入場済みor未入場)が変わらないまま表示されてしまうことがあったのでランダムな4桁の数字をGETでくっつけることで無理やり解決しました。

キャッシュについては本質的解決にはなっていないように見えるし、なんせ美しくない。そしてゲストIDを見えるGETで送ることはセキュリティとかの観点から怪しい気もします。ここは全体的に微妙な仕様となってしまいました。

以下に参考元の記事とその記事のコードにカメラの切り替えと遷移のプログラムを追加したコードを貼っときます。
https://zenn.dev/sdkfz181tiger/articles/096dfb74d485db

JavaScriptコード

Promis、await asyncをあまり理解していないが、参考になるならどうぞ。

async function getFrontCameraId() {
	// メディアデバイスの利用許可を取得
	const stream = await navigator.mediaDevices.getUserMedia({
		video: {
			facingMode: 'environment'
		}
	});
	// メディアストリームのトラックを取得
	const track = stream.getVideoTracks()[0];
	// トラックの設定から deviceId を取得
	const deviceId = track.getSettings().deviceId;
	// ストリームを停止
	stream.getTracks().forEach(track => track.stop());
	return deviceId;
}

window.addEventListener('DOMContentLoaded', function () {
	// カメラ情報を取得して、localstrageに突っ込んでおく
	const devices = navigator.mediaDevices.enumerateDevices()
		.then(function (data) { // promise
			var cameraIdArr = [];
			for (var i = 0; i < data.length; i++) {
				if (data[i]["kind"] == "videoinput") {
					cameraIdArr.push(data[i]["deviceId"]);
				}
			}
			localStorage.setItem('cameras', JSON.stringify(cameraIdArr))
		});

	if (localStorage.getItem('use_camera') != '' && localStorage.getItem('use_camera') != void(0)) {
		if (JSON.parse(localStorage.getItem('cameras')).indexOf(localStorage.getItem('use_camera')) != -1) {
			scan(localStorage.getItem('use_camera'));
		} else {
			// 初期カメラをlocalstorageに登録
			getFrontCameraId()
				.then(deviceId => {
					var use_camera = deviceId;
					this.localStorage.setItem('use_camera', use_camera);
					scan(use_camera);
				});
		}
	} else {
		// 初期カメラをlocalstorageに登録
		getFrontCameraId()
			.then(deviceId => {
				var use_camera = deviceId;
				localStorage.setItem('use_camera', use_camera);
				scan(use_camera);
			});
	}
})

function scan(deviceId) {

	let video = document.createElement("video");
	let canvas = document.getElementById("canvas");
	let ctx = canvas.getContext("2d");
	let msg = document.getElementById("msg");

	// deviceId設定
	userMedia = {
		video: {
			deviceId: deviceId
		}
	};

	navigator.mediaDevices.getUserMedia(userMedia).then((stream) => {
		video.srcObject = stream;
		video.setAttribute("playsinline", true);
		video.play();
		startTick();
	});

	function startTick() {
		msg.innerText = "ローディング中";
		if (video.readyState === video.HAVE_ENOUGH_DATA) {
			// ビデオサイズの調整 (長い方はCSSの方でoverflow:hiddenされる)
			var basisSize = 300;
			if (video.videoHeight >= video.videoWidth) {
				canvas.width = basisSize;
				canvas.height = (basisSize * video.videoHeight) / video.videoWidth;
			} else {
				canvas.height = basisSize;
				canvas.width = (basisSize * video.videoWidth) / video.videoHeight;

			}
			ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
			let img = ctx.getImageData(0, 0, canvas.width, canvas.height);
			let code = jsQR(img.data, img.width, img.height, {
				inversionAttempts: "dontInvert"
			});
			if (code) {
				drawRect(code.location);
				msg.innerText = "QRコードを検出"
				display(encodeURIComponent(code.data));
			} else {
				msg.innerText = "QRコードを検出中...";
				timeId = setTimeout(startTick, 50);
			}
		} else {
			timeId = setTimeout(startTick, 50);
		}
	}

	function drawRect(location) {
		drawLine(location.topLeftCorner, location.topRightCorner);
		drawLine(location.topRightCorner, location.bottomRightCorner);
		drawLine(location.bottomRightCorner, location.bottomLeftCorner);
		drawLine(location.bottomLeftCorner, location.topLeftCorner);
	}

	function drawLine(begin, end) {
		ctx.lineWidth = 4;
		ctx.strokeStyle = "#FF3B58";
		ctx.beginPath();
		ctx.moveTo(begin.x, begin.y);
		ctx.lineTo(end.x, end.y);
		ctx.stroke();
	}
}

function changeCamera() {
	// 現在使用中のdeviceId取得
	var now_use = localStorage.getItem('use_camera');
	// カメラ一覧取得
	var cameras = JSON.parse(localStorage.getItem('cameras'));
	// 存在確認&何番目か
	var indexOf = cameras.indexOf(now_use);
	if (indexOf == -1) {
		// 接続なしの場合
		getFrontCameraId()
			.then(deviceId => {
				var use_camera = deviceId;
				localStorage.setItem('use_camera', use_camera);
				window.location.href = window.location.pathname;
			});
	} else if (indexOf + 1 == cameras.length) {
		// 配列の一番最後のidだった場合
		var use_camera = cameras[0];
		localStorage.setItem('use_camera', use_camera);
		window.location.href = window.location.pathname;
	} else {
		var use_camera = cameras[indexOf + 1];
		localStorage.setItem('use_camera', use_camera);
		window.location.href = window.location.pathname;
	}
}

function display(uuid) {
	// ランダムな数字生成(キャッシュ対策)
	var random = Math.floor(Math.random() * (9999 - 1111) + 1111);
	var nowUrl = window.location.pathname;
	window.location.href = nowUrl + '?u=' + uuid + '&r=' + random;
}

ゲスト情報削除

今年度の文化祭は保護者のみに入場を制限しているので、フォームで送られてきた内容を教員が保護者かどうかをチェックします。もし保護者以外の情報が登録されていたり、重複して送信されていたりしたらデータベースから該当ゲスト情報を削除するようにしました。(この制度は消え去りました。次セクションで詳細)
削除はスプシに保存されているメールアドレスまたはゲストIDを使って削除ページから該当ゲストを検索し、削除する流れにしています。
そしてゲスト情報を削除してしまうと該当ゲストに発行されたQRコードでは入場できなります。間違えたゲスト情報を削除してしまうとけしからん事になる未来は容易に想像できるのでしっかり確認画面もつけておきました。
そしてこの機能はゲスト検索ページを少しいじっただけのものです。

検索結果一覧(これはメールアドレスで検索している)

確認ページ

ゲスト情報無効化

この機能はゲスト情報削除機能が作られた後に実装されました。
同じ情報で2回以上事前申し込みを行った場合、原則前に送られた情報を削除するようにしていました。しかし、もし当日削除された方のゲスト情報が入ったQRコードを提示されたらどうでしょうか。「存在しないゲスト情報です」とエラーが表示され、ゲストの不正を疑わなければいけなくなります。
それを防ぐため、ゲスト情報無効化では情報をデータベースから完全に削除してしまうのではなく、DB(guest)のstatedeletedにし、そのQRコードが読み込まれた場合は、「無効化されたQRコードです」というエラーを表示させることである程度意味が分かるようにしました。
操作方法はゲスト情報削除とほとんど代わりません。

Crackerアカウント・ログイン

Crackerのユーザーアカウントは管理者アカウントと一般アカウントの2つに分けました。
一般アカウントでは入退場スキャン、登録情報の確認ができます。管理者アカウントではCrackerアカウントの作成や新規展示情報の作成、ゲスト情報の削除などが行えるようにしています。
学校で使用する目的のシステムであるためユーザーは管理者が一括で管理するようになっています。

またそれぞれのユーザーには各々持ち場(展示)を設定し、そこでのみ入退場の処理を行わせるようにしました。それによりDB(activity)にデータを入れる時にどこに入場したかを記録することができます。もっとも今回は入場口しか無いのであまり意味は無いですが。

またログイン/ログアウトの処理はセッションを利用して行っています。

ユーザー登録ページ

展示情報

ゲストがどこの展示会場を出入りしたかを記録する際に必要となる展示情報です。展示登録ページで追加した展示情報をユーザーに紐付けることができます。

展示情報登録ページ

アナリティクス

現在の入場グループ数と入場人数(理論値)を取得し、表示させました。
ゲスト情報はひと家族ごとに登録されているので入場人数は登録されている全員が入ったときの理論値となっています。

とてもシンプルなアナリティクスページ

送信用メール生成機能

運営記で詳細を書きますが、文化祭2、3日前に今回の運用上一番大きいと思われるトラブルが発生しました。一時的にフォームで送信された情報がスプシにはきちんと登録されているのに、データベースへの登録とメールの送信が正常に行われていなかったのです。GASに関するエラーと思われますが、原因は不明で時間が経つと治っていました。
そこで正常に処理が行われていないゲスト情報が30, 40件ほどありました。そのゲストに手動でメールの送信を行いました。
手動とはいっても、こっちもプログラマーなのでメールアドレスを入力すると、送信すべきメールの内容を表示するプログラムを急遽作り、簡単に対応できるようにしました。

生成されたメール本文

新規ゲスト登録

この機能も先述したGASのエラーのために手動でゲスト登録をするために実装しました。ただただフォームが表示され、それに沿って情報を入力していけばデータベースに新規ゲスト登録が行われるものです。気をつけなければならないことは、ここで追加した情報はスプシ上には反映されないということです。

ゲスト用QRコード表示サイト

メールで画像を送信するだけでは外部コンテンツの読み込みの関係で表示できなかったり、メール内で外部画像をダウンロードするのはめんどくさそうだったのでQRコードを表示・ダウンロードするだけの簡単なサイトを作成しました。
基本的にはGETパラメータにゲストIDを乗っけて、その情報をもとにQRコードを表示しています。なお1回アクセスするとCookieにゲストIDが30日間は保存されるため、一度でもアクセスをしているとCookieの情報によりQRコードを表示できます。

QRコード表示サイト

開発記

覚えている限り開発の一部始終のよしなしごとをそこはかとなくかきつけていこうと思います。

GW前、開発のお達し

学年が上がって2回目の情報の授業の後、突然情報科のK先生から話しかけられました。文化祭の入退場システムを作らないか?と。
僕が自己紹介欄に趣味が競技プログラミングと書いた(その時の2週間だけしてた)からなのか、他の先生に教えて貰ったのか知りませんがOKをしてシステム開発担当になってしまいました。

※後日聞いたところ、自己紹介欄に書いたのを見ただけで話しかけてきたようです。謎の行動力。

数日後K先生と目的や主要機能などの話し合いを行い、作れそうだったので本格的に開発を始めました。この時点で開発に費やせる時間は1.5ヶ月ほど。デバッグや関係者への周知の期間も必要なので実質1ヶ月程です。

3日後...

開発期間が極端に短いことは初めから分かっていたので、話し合い後すぐの土日で色々とシステム設計を詰め、主要な機能は一気に実装をしました。
当初はスプシで全てのデータを管理するつもりでしたが、明らかにダルそうだったのでMySQLを使う仕様に勝手に変更しました。やはりデータベースはSQLに限ります。

この時点でフォームに関してはフォーム自体の作成、データベースへの登録、メールの送信を。Crackerに関してはログイン/ログアウト、QRコードスキャン機能を実装し終えました。

その後、K先生+文化祭担当の先生に作ったプロトタイプをお披露目し、良さげな反応を頂いたので必要機能を更に洗い出しGWに突入です。

GWの怒涛の開発

GW後には学校のテストが近づいてくるのでそっちの方の勉強をしなければならないので、GW中にほぼ全てを完成させるつもりで頑張ります。
GWで長時間開発時間が取れたことにより気合でほぼ全ての機能を作り終えました。今後はここで作ったものがベースとなって機能の追加や修正が行われていきます。

この時にドキュメントを書き始めて、それの一部が色んな先生にシステムの説明をする原議書(?)になったり、この記事の作成の土台にもなっています。そしてこの記事もドキュメントの一つとなるでしょう。

唯一残っている初期のスクショ

サーバーの契約

PHP+MySQLで構成されているのでそれが使えるレンタルサーバーを探します。(とはいってもほとんどのサーバーで使える)
色々検討したのですが、最低契約期間が2ヶ月、3ヶ月と長かったりするサービスが多かった印象です。最終的にcolorfulboxが30日間無料で契約できるということで費用を抑えるためにもこれを使うことにしました。
契約後に気づいたことですが、colorfulboxだと複数のユーザーでサーバーを管理することが出来るので便利でした。

テスト・デバッグ

気合のデバッグです。事前申込、当日の入場スキャン共に同時に複数の処理が実行される可能性があるので、同時に複数台でスキャンしてみたり、winのPower Automate Desktopを使って同時にフォームの送信テストなどを行いました。
他記事にもありましたが、SQLの同時接続数の確認とか重要!

家で同時アクセステストしている様子

公開!

まずは申し込みフォームの公開です。公開は突然やってきました。
「今日帰ってからフォームの最終テストをしよう!」と意気込んでいた日の掃除時間、Mr.Kが急に自分のところに来た気がします。

K先生:「なんかフォームが今日公開されて、申し込みが開始するらしい」
自分:「??!”??!?!?”??!?(まだ最終確認終わってない)」
その場に居合わせた先生:「(心配そうな目で)今日配布しても大丈夫なの?」

終礼で事前申込とフォームのリンクが記されたプリントが配られ、若干焦りつつ放課後、パソコン室で最終確認を行いました。その日に10、20人の先生方にテストのためにフォームを送信してもらい、大丈夫なことが確認が取れていたのである程度は大丈夫でした。なんとかセーフ。

webシステムの方もサーバーの契約が済んだ位でベータ版として公開し始め、どんどんアップデートを行っていきました。

着々とアプデされていくCracker

運営記

事前申し込みフォーム

トラブル1 ~忘れられたメール送信制限~

Googleフォーム×スプレッドシートを使うのでGoogleアカウントが必要です。通常のGoogleアカウントでは一日にGASで送ることができるメールの数は100件に制限されています。幸いなことに学校でGoogle Workspaceを契約しているのでそのアカウントを使用するとメールの送信制限は2000件まで拡張されます。
100件までだと心もとないので当初は学校のアカウントを使用する予定でした。

後日学校アカウントでGASを動かそうとすると、謎のエラー(権限関係?)でGASのトリガーを設定できません。担当の先生に権限を貰ったのですが、それでも解決に至らず。
ここで余計に意味不明なのは、K先生の学校のGoogleアカウントを使用すると正常にトリガーを設定出来るのです。意味がわからない。

まぁ原因不明のエラーに対処は無理そうだったので、通常のGoogleアカウントを作成し、そっちの方でGASを動かすようにしました。
そうです、皆さんお気づきでしょう。おい、さっきのメールの制限とやらはどこに行ったんだよ?と。その時完全にメール送信制限のことを忘れていました。

そして、肝心のフォーム公開後。見事に送信件数が100件を超えてしまいました。
しかし、奇跡的にその1日に来たフォームの数は101件。エラーは1件だけで済んだのです。その人には多分紙にQRコードを印刷して渡したような気がします。少しでも多かったらエラーがもっと増えたことでしょう。Got kotonaki!

トラブル2 ~何故か処理されないGAS~

文化祭数日前の夜中11時にこのエラーに気づきました。
半日分、約20件分のフォーム送信データがデータベースに登録されず、メールも送信されていないのでした。原因を軽く調査するも不明。なんでこんなにGASは意味不明なエラーがあるのでしょうか。
その夜の内に急遽、Crackerに新規ゲスト登録機能を追加し、データベースに情報を登録しました。

夜11時にClassroomで騒ぎ始める人の図

翌1時位まで作業は続き、残るはQRコードをどう発行するかです。夜中K先生とやりとりをした結果、翌日の朝に一気にQRコードを貼り付けたメールを送信する作戦を取ることにしました。
翌朝、急ピッチでメールアドレスを打ち込むと送信するべきメールの内容を生成してくれる送信用メール生成機能を作成しました。(15分位で作れたのは天才)
学校へ早く行き、先生と2人で夜中から増えていた分も含め20~30件程コピペしては送信を繰り返しどうにかなりました。

ちなみに、その謎のエラーは翌日の朝8時位には正常に処理されるように戻っていました。原因不明ほど怖いものは無いですね。

Cracker

Crackerに関する資料配布

受付担当の生徒(先生も?)には当日Crackerを使用する旨が書かれたプリントが配られました。そこにCrackerのユーザーIDとパスワードが載っていたのですが、そこに載っていたアカウントがなんと管理者アカウント。んんん?!?!
受付用アカウントが存在していたのですが、担当の先生の勘違い?により管理者アカウントが配布されていました。管理者アカウントはゲストの削除など重めの機能がついているので、一般生徒にアクセスされては困るということで急遽ユーザーIDがadminながらも管理者権限を剥奪しました。adminとは...

事前講習

受付担当になっている教員と生徒を集め、当日の流れやCrackerの使い方など事前講習を行いました。前日に。前日にやるのは良くないって話は反省点の方に書いておきます。
事前講習では受付シフトの話やCrackerの使い方の説明などを担当の先生が行いました。僕は横で突っ立っていました。この日Crackerの画面を見せるのにプロジェクターなどを用意していなく、ノートパソコンの画面を20,30人程が囲んで見ていました。良くないですね。事前講習ではあまり実感を掴んでもらえなかったような気がするのは自分だけでしょうか。

当日

ゲストの受付・入場は9時スタートでした。9時前に入場口へ行くと、思いの外入場待ちの人が多くいました。
前日実際にCrackerを触ってもらうことは無かったので不安でしたが、受付の生徒はスムーズに入場処理を行え、最大で8件/分で処理が行われていました。受付レーンは2つ、文化祭パンフを渡す業務も同時に行われていたのを考慮するとよい結果だと思います。

当日QRコードが表示できない/申し込みを忘れた人は生徒の保護者に限り、Googleフォームで登録または申し込み用紙を用意していたのでそれに諸々の情報を記入してもらい入場させました。
Gogoleフォームは回答を1回に制限するためにGoogleアカウントでログインしてからフォームの入力が行えるようにしていたのですが、想像以上にログインでもたつく人が多かった印象です。途中から1回の制限を撤廃し、なんとかその問題は解消できたのではないかと思います。

そして当日は保護者以外の一般の方も数人来ました。保護者のみの入場に限られていると伝え、残念ですが帰ってもらうことで対処しました。

再入場を希望するゲストに関しては、事前に作っておいた再入場用のリストバンド(紙に印刷しただけ)を腕につけてもらい再入場の際に見せてもらうようにしました。

当日特に大きな問題は発生することなく終えられて良かったです。ちなみに事前申し込みに対する実際の入場数は8割程でした。

反省点・改善点

この記事においてここが一番重要まである。PDCAサイクル重要

事前申し込み

すぐにメールが届くことを明記しておく

当日、メールが届いていないのに、来場する保護者が複数いました。メールが届かず学校に電話等をしてくださった方には紙媒体でQRコードを発行し対処出来るのですが、何も言われなかったらどうしようもない。
おそらく、すぐにメールが届くとは知らず、申し込みだけして満足してしまったのでしょう。そのためフォームを送信するとすぐにメールが届くことを明記し、届かない場合には連絡をしてくれと分かるように書いておくべきでした。

ゲスト向けの注意事項やQ&Aを表示する

一つ前のセクションと被るところもありますが、事前申し込み等に関しての注意点をまとめておくべきでした。メールが届かない場合は迷惑メールボックスを見てみるなど、書いておいたほうがスムーズに事前申し込みができたでしょう。

来場を家族のみに制限していることを一般の人にも分かるように

入場を家族に限定していたにも関わらず、家族ではない人が何グループか来場しました。そのため学校HPなどで一般の人の入場は制限されていることを明記しておく必要が合ったかもしれません。

Cracker技術面

ゲスト検索を名前でも出来るようにする

「システム設計・機能と詳細」で先述した通り、Crackerでは登録されているゲストの情報をゲストID(uuid)またはメールアドレスから検索できるような機能がありました。
ですがよく考えてみると、ゲストIDまたはメールアドレスが分かっている状況は当日運用している中であまりありません。

来場者がQRコードを表示できないときに、ゲスト情報を検索する必要があったのですが、ゲスト名で検索できるととても楽です。当日は送信済みメールやフォームと接続してあるGoogleスプレッドシートでゲスト名を検索することでQRコードが表示できない人の対応を行っていました。

ゲスト検索画面で入場処理を行えるようにする

これは一つ前の名前検索とセットで実装して、本領発揮するのですが、ゲストを検索した画面で直接入場処理ができるようにすることです。

今回実装した方法ではQRコードを表示できない人への対応は以下のようになっていました。

  1. スプレッドシートでゲスト名を検索し(ctrl+F)、メールアドレスをコピーする
  2. ゲスト情報検索画面でメールアドレスからゲスト検索をし、ゲストIDをコピーする
  3. 入場スキャン画面でコピーしたゲストIDを貼り付けて、入場処理

うん、普通にだるすぎる。

本番運用中に送信済みメールから名前を検索して、送信されたQRコードをパソコンで表示され、他のパソコンで読み取って処理するという方法が発見されましたが、どっちにしろ2度、3度手間となり、めんどくさすぎます。

ということで理想では以下のような流れにするべきでした。

  1. ゲスト情報検索画面でゲスト名によりゲスト情報検索
  2. ページはそのままで入場処理ボタンを押して処理完了!

まぁ、なんとシンプルで分かりやすい。(文化祭後に実装し直した。)

ログをもっと記録する

Crackerでは入退場スキャンを行った時に不適切なQRコードを読み取るとエラー表示が行われます(当たり前)。以下入場時のエラー一覧。

エラー文言 状況
文化祭用のQRコードではありません DBに存在しない情報が書き込まれたQRコードが読み込まれた
既に入場済みです。 既に入場スキャンを行ったQRコードが読み込まれた
無効化されたQRコードです。 事前申し込みで重複申し込み等が原因で学校側(管理者)がゲスト情報を無効化したQRコードが読み込まれた

これらのエラーが表示された場合、そのログを取っておくべきでした。文化祭後に眺めることができるデータが多くなって楽しい、という理由もありますが、ログがあれば何か改善策等が見えてきたかもしれません。

ログイン保持が不完全実装

Crackerはユーザーがログインを行って利用します。ログイン情報はセッションで管理されているのですが、セッションの保存期間の設定がイマイチなまま本番になってしまいました。当日トラブルは発生しなかったものの、開発者としては納得はいきません。きちんと設定するべきでした。

ゲスト情報編集機能をきちんと実装する

Crackerではゲスト情報(来場する人の続柄と名前)が変更できるように急遽編集機能を作成しました。時間の都合上きちんとしたフォームを作ることができず、配列形式で変更後のゲスト情報を入力しろというゴミフォームができてしまいました。自分が何回か使っただけです。きちんと作るべきですね。

配列形式での入力しか対応していないゴミフォーム

事前講習

少なくとも一週間前くらいには事前講習会を行う

今回受付担当の教員及び生徒に事前講習・説明がなされたのはなんと前日!これは良くなかったですね。生徒は入場処理だけを担当するのでまだいいにしろ、エラー対応を行う教員にはまともに説明ができないまま当日を迎えてしまいました。
そのため、ほとんどの教員はエラー対応が当日対応可能な状態に無かったため、ほぼずっとK先生か自分が受付にいました。

実際にCrackerを触ってもらう

前日に説明会を開いたせいで教員と生徒は事前にほとんどというか、全くCrackerに触れていません。これは良くない。事前に触れておくことはもちろん、なんなら本番の場所で本番を再現してデモンストレーションを行うべきでした。

周知・周知・周知

事前に関係者に周知しておくことは非常に大切だということを思い知らされました。事前にドキュメントやマニュアル等は書いたものの配布が前日・当日になってしまい、あまり読まれていないと思います。特に教員へのエラー対処法の周知はしておくべきでした。また生徒にも「既に入場済みです」などのエラーが出た際の対応を言っておくべきでした。
当日無効化されたQRコードを読み込み、普通に入場させていたのでしっかり対応が必要でした。

エラー対応

楽を求めるエラー対応

やはりエラー対応も楽で有るべきです。
今回の一番大変だった雑務作業といえばフォームを送信して、メールアドレスの打ち間違え等によりメールが正常に送信できないゲストへQRコードを印刷して配布することだと思います。
要エラー対応ゲストが一覧になったリストには40件程エラーが必要なゲストが並んでおり、そのほとんどがメールが受け取れていないというものです。
このエラーの対処法は作成したQRコード生成APIでQRコードを生成し、それを印刷して生徒経由で保護者に渡すというものでした。この作業はほとんど先生が(多分)手作業で行っていました。

さっさとメールアドレスを入力したら、配布用のpdfが生成される機能でも付けたら良かったです。

エラー対応の一意化

一つ前では事前のエラー対応でしたが、これは当日のエラー対応です。
「事前申し込みをしたけど、メールが見当たりません。」というのを何度聞いたことでしょうか。
この場合の対処法はスプレッドシートから検索や送信済みメールから検索など色々な方法がありました。ですがエラーを対処する教員からしてみれば、複数種類があって分かりづらかったりしたことでしょう。しかもどの方法も2,3個処理を挟む必要があり非常にめんどくさいです。(詳しくは「反省点・改善点/Cracker技術面ゲスト検索画面で入場処理を行えるようにする」を参照)
そのため、分かりやすく、楽にするためにも対応方法を1つにまとめる必要があったかと思います。

おわりに

今まで色々なwebサイト等を作り公開してきましたが、そこそこの規模感のしっかりしたサービスを作るのは初めてで不安な部分もありました。特にデータベース周り。
しかしなんとかなり、当日も大きな不具合が起きることはなく無事に終えることができて感動です。また本番を想定した設計やユーザー目線の設計の重要さが分かりました。高校生のうちに貴重な体験ができたと思います。
Cracker導入にあたり、声をかけてくれた先生や文化祭担当の先生、使用許可を出してくれた寛容な学校、当日スムーズに運用をしてくれた生徒・教員等色々な人に感謝です。

また今回の実装でセキュリティ関係についてまだまだ知識が足りないと思い、これからしっかり勉強していきたいです。(後日気がついたがSQLインジェクションの脆弱性あった)

いよいよ技術的なこととは関係がなくなってくるのですが、開発者用ドキュメントやユーザーズマニュアル、トラブルシューティング、そしてこの記事だったり、プログラムと同じくらいテクニカルライティングをしていました。めっちゃムズい。
書いていると自分は理解しているけど、「この文章で他の人に伝わるのか?」という気持ちによく陥りました。テクニカルライティングも今後しっかりできるようにならなくてはいけませんね。

最後まで読んでいただきありがとうございました!

Discussion