エンジニア夫婦が自分たちの結婚式用にプロフィールサイトとウェブアプリを作った話

# はじめに

私事ですが今年の6/27に結婚式を挙げました。

私も奥さんもエンジニアなので何か二人らしいおもてなしができないかと思い、よくある席次表プロフィール代わりのプロフィールサイトと披露宴のレクリエーション用にクイズウェブアプリを作成しました。

# 作ったもの

分担は、奥さん（@tasopi）が主にプロフィールサイトのコーディングとデザイン作成を担当し、私（@danishi）はアプリのフロントエンドとバックエンド、AWSインフラ周りを担当しました。

結婚式の準備の合間を縫って工期は２ヵ月くらい。

実際作ったものの当日の写真が以下です。

## プロフィールサイト
席次表に印刷しておいたQRコードから遷移して見てもらうようにしました。
トップページとプロフィール紹介、自分年表のようなページで構成されています。
![profile.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96507/d379db6b-e2c6-cf1b-5561-b14300b76314.jpeg)

## クイズアプリ（出題側）
予めURLを伝えたスタッフさんに操作してもらうPC用画面で、順番に問題を出題、最後に問題の正解とランキングの発表ができます。
プロジェクターに映してゲストには見てもらいます。
事前に操作手順書を作って司会者さん、スタッフさんにお渡ししました。
![quiz_admin.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96507/857d4359-ec3b-169f-871e-a90db628d28c.jpeg)
![answer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96507/2a70182b-a7d9-2160-3202-9a41e13f968f.png)

## クイズアプリ（回答側）
ゲストに操作してもらうスマホ用画面です。
出題側の初回画面で映したQRコードから遷移してもらいます。
初回のニックネーム登録を終えると、後は画面に出ている問題に合わせて三択を押すだけです。
![quiz_client.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96507/ac16f1a6-724c-c5f5-bb9d-d01e2aa4cc28.jpeg)

# アーキテクチャ
AWSを使って流行のサーバーレス構成で作っています。
利用者は結婚式の出席者だけなので同接60人にも満たない規模でしたが笑。披露宴中も負荷に耐えきり、心配してたエラーやダウンもなく使えました。
ランニングコストもドメイン代抜いて$10いかないくらい。

## AWSインフラ構成図
![構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96507/eb5aec3b-311a-b7e0-adce-e192f83fc86f.png)

### フロントエンド
プロフィールサイト(React)、クイズアプリ(Nuxt.js)ともにフロントエンドはCloudFrontでS3に置いた静的ファイルを配信しています。
公開当日まではWAFでIP制限をかけていました。
出題者の画面は決まった人しかアクセスできないようにLambda@EdgeでBasic認証をかけています。
GitHub Actionsを使って、GitへのPushを契機にソースのビルド、S3へのデプロイができるようにしています。

### バックエンド
クイズアプリに必要なバックエンドはAWS Chaliceフレームワークを使っています。
`chalice deploy`コマンドでAPI Gateway、Lambdaをデプロイできます。
DynamoDBの操作には、[PynamoDB](https://github.com/pynamodb/PynamoDB)を利用しています。

# クイズアプリの設計
バックエンドが必要なクイズアプリはそこそこ気合を入れて設計しました。
なるべくゲストの操作を簡単にして老若男女が遊べるように工夫しています。

## テーブル設計
DynamoDBを使っているもののそんなにベストプラクティスに沿ってはいません…。
ユーザーがそんなにいないのである程度力押しで手抜きしてる部分があります。

### ユーザーテーブル
回答するゲストの情報と成績をセットするためのテーブルです。
回答画面でゲストにニックネームを初回登録してもらいます。

| 属性               | 型     | 説明            |
|:------------------ |:------ |:--------------- |
| user_id（パーティションキー）| String | 回答するゲストを識別するUUID。 |
| nickname           | String | ゲストに入力してもらう名前。結果発表に使います。  |
| before_question_id | Number | 同じ問題を二度回答させないために前回回答した問題IDを保存。 |
| score              | Number | 回答成績。初期値はゼロ。正解するとカウントアップします。|

### クイズテーブル
予めセットしておく問題のテーブルです。
出題画面で内容を表示。
ゲストが答えた回答の答え合わせにも使います。

| 属性               | 型     | 説明            |
|:------------------ |:------ |:--------------- |
| question_id（パーティションキー）| Number | 問題のID。 |
| description        | String | 問題文。 |
| correct_answer_id  | Number | 正解の選択肢の番号。 |
| answers            | List (String) | 選択肢のリスト。 |
| stat1              | Number | 選択肢1を選択したユーザーの数 |
| stat2              | Number | 選択肢2を選択したユーザーの数 |
| stat3              | Number | 選択肢3を選択したユーザーの数 |

### 現在クイズテーブル
現在出題画面でどの問題を出しているかをセットするためのテーブルです。

| 属性        | 型      | 説明         |
|:----------- |:------ |:------------ |
| key（パーティションキー）| String | 固定値をセット。ホットスポットになるのであまりよくない使い方です。|
| question_id | Number | 現在の問題IDが入る。                  |

## API設計
使うAPIは全部で5本。
フロントエンドのNuxt.jsではVuexストアに情報を保存しながらaxiosでAPIを呼び出します。

### 回答側
#### `POST /user/register`
初回のユーザー登録の処理を行います。
ニックネームをパラメータにユーザーテーブルに登録。
ユーザーIDとしてUUIDを払い出します。

#### `GET /user/answer/{user_id}/{answer}`
ユーザーの回答を受け取るためのAPIです。
現在出題されている問題を取得し、答え合わせ、自身の成績に反映します。

### 出題側
#### `GET /admin/question/{question_id}`
出題画面に出す問題を取得するためのAPIです。
これを呼びだすことで現在の問題を設定します。

#### `GET /admin/score`
出題画面で最後のランキング発表に使います。
ユーザーテーブルをスキャンして成績順に並び替えて返します。

#### `GET /admin/correct_answers`
出題画面で最後の正解の発表に使います。
クイズテーブルをスキャンしてそれぞれの問題の答えを返します。

# 実際のクイズの流れ
1. 出題画面を開いて初期画面をプロジェクターに投影。
1. 表示されているQRコードを読み取るよう司会者からアナウンス。
1. 読み取ったゲストは回答画面の指示に従ってニックネームを登録して待機。
1. 司会者がゲストの準備が整ったことを確認して出題開始。
1. 司会者がプロジェクターに表示された問題を読み上げ、ゲストはそれを見て回答。
1. これを10問繰り返す。
1. 最後に結果を発表して上位3名に景品をプレゼント

# 改善点
あとでゲストに感想を聞いた結果以下のような改善点がありました。

* プロジェクターで写すことを十分想定していなかったので文字が若干小さく読みづらかった
* プロジェクターの問題に対して回答するというルールが十分に周知されておらず、自分がどこの問題を回答してるかわからなかったというゲストがいた。一本APIを増やしても回答側に問題を表示していた方が分かりやすかったかもしれない。

私たちはもうやり直しはできないですが笑、似たようなことをやろうと思った方は参考にしてみてください。

# やってよかったこと
お互い経験のある言語や領域が異なっていたので、それぞれの得意なところをカバーしながらお互いのスキルアップが図れたのがよかったです。
結婚式の準備に加えて開発は大変でしたが、出来上がったときの達成感はひとしおで、当日無事みんなに喜んでもらえたのは嬉しかったです。

# 謝辞
最後に、コロナ禍の結婚式にもかかわらず足を運んでくださった親族、友人。画面の操作やクイズの司会進行にご協力いただいた会場の[表参道TERRACE](https://www.tgn.co.jp/hall/omotesando/ot/)のスタッフの皆様への感謝とともに、本記事の締めくくりとさせて頂きます。