お祭りの屋台で焼きそばを買ったとき「少し待ってください。」と言われた。
どれくらい待てばいいんだろう?
結局30分待った。この間にVRジェットコースター乗りに行けたな...
私たちが文化祭で経験したこんな出来事から「待ち時間」と「待ち順」に注目し、開発をしたのが「マチケンナンバ」です。
お店、イベントの主催者、お客さんのそれぞれの目線に立ち、操作が簡単で、導入コストを極限まで低くしたアプリの開発を目指しました。
はじめに
12月初旬から中旬にかけて、私が所属する団体では、ピクシブ株式会社様および団体のOBの皆様の多大なご協力のもと、今年も無事にハッカソンを開催することができました。
各分野の専門的なメンターをご用意していただき、不明な点がある際には迅速にご指導いただける環境を整えていただきました。この場をお借りして、深く感謝申し上げます。
友人ら3人とともに開発した、お祭りでの待ち時間の悩みを解消するアプリ「マチケンナンバ」が、このたび見事にpixiv様から最優秀賞、OBの皆様から優秀賞を受賞する栄誉に預かりました。
この記事では、そんな「マチケンナンバ」の概要、制作背景、そして工夫した点(ビジネス的な観点から技術の詳細まで)について、順を追って分かりやすくご紹介していきます。プログラミングにあまり触れたことがない方でも楽しめる内容となっておりますので、ぜひ最後までお読みいただければ幸いです。
1.「マチケンナンバ」の概要
あるお祭りで焼きそばを2つ買ったサトシくん。
後で商品と引き換えるための番号札をもらい、その番号が呼ばれるまで待つことに。
しかし、番号がいつ呼ばれるのかわからないため、なかなか近くのお化け屋敷に行くことができません。
2時間後も目の前で待ち続けるのは時間の無駄ですよね。
今回の例は実際の自分の大学の学祭で起こっていた問題であり(わかるように少し誇張しましたが)、次期学祭委員長にもお話を伺うことができました。
(大学のお祭りでは)一本道で出店を行なっていることもあり、買った後に待っている列が大きすぎることでごちゃごちゃしてしまう。
そこで、私たちはこの悩みを解消すべく、お店ごとに待ち時間を取得でき、自分の待ち時間が簡単にわかるアプリを開発いたしました。
このお店ごとの待ち時間というのはディズニーランドやユニバの待ち時間管理サイトのような形であり、それぞれのお店の待ち時間がわかります。
また、このような機能の実現により、お客さん、イベント、お店がそれぞれの需要を満たせる、まさにwin-win-winの状態を実現することができました。
このアプリの一番の特徴は、売れているお店、売れていないお店に関係なく、すべての店舗に導入するメリットがある点です。お客さん、イベント主催者、お店が相互作用し、互いにメリットを享受できる仕組みが成り立っているため、どの店舗にとっても価値があるシステムになっています。
では、いったいどういうアプリケーションなのでしょうか。
一つずつ見ていきましょう。
登録
登録の際はイベント・ショップか選んでもらい、すぐにご利用いただけます。
プロフィール編集や、パスワードを忘れた時はメールアドレスでリセットできるなど、幅広いニーズに応えれるよう調整いたしました。
お店の管理
まず、メニューの登録はメニュー欄から直感的に行うことができ、オーダーは即座に反映されるようになっています。
また、後からこのメニューの編集と消去も可能にしております。
先ほど登録したメニューを使って実際にオーダーを作っていきます。
オーダーの状態はカードを渡して準備中の"未完了"、お客さんに渡準備ができた"完了"、そもそも使っていない"未使用"の3つのステータスがあります。
ここで新しいオーダーを取る時、未使用のものを使います。
完了するを押すことで、完了したものと未完了の一覧を把握することができます。
また、この時準備中(渡しているカード)を間違って使用しないように、ヴァリデーションもしっかりとしています。
イベントの管理
イベントの登録手順は先ほどと同じです。適当に作ってみました。
ここにイベントとお店を繋げていきます。
イベントの方にもらったイベントコードをショップのところに打ち込んでもらうことで、繋ぐことができます。
店舗状況とオーダーの閲覧(お客さん)
最後に、お客さんがイベント一覧とオーダー情報を見る必要がありますね。
これは簡単です。イベント一覧から探すことで、お店を見つけることで、そのお店の待ち時間がすぐわかるようになります。
中身は先ほど見せたこちらの画像ですね。それぞれの時間がすぐわかるようになっています。
お店の数が多いことも想定できるため、お店にすぐ行けるようにお店のQRコードを発行する機能もつけました。
「マチケンナンバ」は、このような仕組みを通じて、お祭りにおける待ち時間の悩みを解消を目指して制作されました。
2.制作背景
メンバーの自己紹介
遅れてしまいましたが、ここでメンバーのご紹介をさせていただきます。
技術選定
今回の使用技術は主にこちらになります。以下の図はひつじくんが作成してくれました。
実際のスケジュール
全てがうまくいくわけもなく、失敗からさまざまな学びや参考になることがありました。ぜひ温かく見守っていただけると幸いです。
1日目
二週間と限られたスケジュールということもあり、初日は何を実装するかなどを話し合いました。
まず、ショップで待ち時間を出し、個人がそれを見て、時間を管理できることを第一の目標とし、次にイベントを実装しようと決めました。
色々な機能をつけすぎてパンクするより、期限内に一つのプロダクトを完成させる方がいいと思ったからです。
また、discord上でも会議はできますが、実際にロールプレイしないとわからないこともたくさんあり、この日がなければ完成していなかったと思います。
そして話し合いの結果、マクドナルドのような待ち時間システムを考案し、必要なものをデータベースのER図を手書きで考えました。
こうやさんが正式な仮のER図としてくれました(仕事早スギィ!)
最終的には全然変わってしまいましたが、初日のルーティング案はこんな感じでした(汚いのは許して)
また、技術選定も同時に行い、どこの分野を誰が担当するのかもこのときに決めました。
帰宅後、それぞれ分野のことを始めました。
そして早速、ひつじくんが開発環境を整えてくれました。(仕事早スギィ!2)
開発のファイル管理はGithub
、連絡はdiscord
、資料などはNotion
で管理することにし、開発用のブランチをいくつか作りました。
いぬかきくんはzipファイルで管理することを提案しましたが、秒で却下されました。
開発初期(2日目~7日目)
いぬかきくんが早速バックエンドを構築していき、APIの仕様書をswaggerで書いてくれました。
それぞれリクエストボディなどもしっかり書かれており、APIを叩く側からすればとてもやりやすかったです。
こうやさんがそれぞれのルートのファイルの骨組み、軽い仕様を書いていきました。
その後、お客さん側はshimaf
、お店側はこうやさん
、event側はひつじ
が担当することにし、それぞれUIを意識せずに ひたすら書いていきました。
私はその間に認証機能をつけるべく、NextAuthをいじくっていました。
初めはバックエンドのAPIが構築されまではNextの中で仮のAPI作ってそこに叩いてました。
おかげでディレクトリ構成が以下の通りぐっちゃぐちゃでした。
開発中盤(8日目~11日目)
ずっと家で開発してたので、いざ大学で試してみたとことろ、謎のエラーが連発しました。
認証自体はNextAuthなのですが、お客様情報は一時的にSupabaseで保存していたので、ローカルホストからアクセスする6543番ポートが学校のwifiによって閉じられていたのです!
それに気づかず環境汚して繋がらなかったのかなぁと模索して危うく1日使うところでした。
中盤はそれぞれの分野に分かれて自分のことに尽力しました。
いぬかきくんがAPIをを完成させたので、Postmanを使って実際に通るかのテストをしていきました。
ここで必要なルートなどの再確認もでき、その都度いぬかきくんに修正をしてもらいました。
あと、気づいたらひつじくんがドメインとっていました。(仕事早スギィ!3)
あと、気づいたらひつじくんがロゴを作ってくれていました。(仕事早スギィ!4)
開発終盤(12日目~最終日)
そして、終盤は近づいてくるゴールとなんとか完成させないといけない重圧が気づいたら楽しさに変わっていきました。
面白いですね、ハッカソンって。
私はイベントとお客さんを繋げる+店とイベントを繋げる+しおり化の作成を構築しなければならなく、おかけで寝れない日々が続きました。(もちろんハッカソン後に倒れました)
そして、こうやさんはインターンなどで鍛えた腕でバグの修正を行なってくれました。
また、フロントからひたすら飛んでくるAPI修正を必殺仕事人いぬかきくんがすぐ修正してくれました。
ひつじくんがトップページを作ってくれました。綺麗にまとまっていていいですよね。
そして完成し、いざ、ビルド!!!!
あれエラーが、、、(スマホでターミナル触るひつじさん)
ビルドできるのに起動ができないだったり、開発者起動できるのに通常の起動ができないなど、色々な事故が起こり、直前までずっと調整していました。
しかし、そんなこともあろうかとローカルでの環境を3ページ出すことも一応準備していたので、ちゃんとロールプレイングのような形式でご紹介することができてよかったです。
結局、本番の発表中にようやく起動できるというなんとも言えない結果になってしまいましたが、一般公開できるところまで持っていったという形にはでききてよかったです。
3.工夫した点
ビジネス面
このようなプロダクトを開発する際には、まずどのユーザー層から収益を生み出すかを明確にすることが重要です。
一般的な収益モデルとしては、広告収入、サブスクリプション料金、売上の一部を収益化する手法などが考えられます。
しかし、このプロジェクトにおいては、以下の理由からこれらが適していないと考えました。
-
利用者(お客さん)からの収益化の難しさ
ユーザーが直接何かを登録する仕組みではないため、広告や利用料を通じて収益を得るのは現実的ではない。 -
店舗側の利用料負担による普及の妨げ
お祭りなど現金取引が主流の場面で、売上の一部を計算して収益化するのは困難である。また、店舗に登録料を課す形式では、なかなか普及しづらい。 -
既存の待ち時間管理システムとの差別化不足
待ち時間表示だけの機能では、AirWAITのような大手の既存システムと大きな差別化が図れない。
そこで、新たな収益モデルを考えました。
提案内容
「マチケンナンバ」の基本機能を無料提供し、付加価値機能で収益化を図る
-
無料版
イベント参加者向けに、簡易的な待ち時間一覧機能を無料で提供する。 -
有料版(プラス版)
待ち時間一覧画面から店舗の詳細情報や画像を閲覧できる充実した機能を提供する。これにより、イベント全体の利便性を向上させつつ、しおりとしての機能をもっと充実することが可能になる。
この仕組みによって、基本機能を無料で利用可能とすることで「マチケンナンバ」の普及を促すのと同時に、有料版でさらなる機能を提供することで、収益を得るだけでなく、サービスの市場拡大につなげることができると考えます。
技術面
サーバーについて
サーバー編に関してはひつじくんが別途記事にして書いてくれました。
👇是非ご覧ください👇
フロントについて
サーバーを除いた技術選定は以下のようになっています。
- 言語:TypeScript
- フレームワーク:Next.js
- CSSフレームワーク:TailwindCSS
- UIコンポーネントライブラリ:Shadcn/ui
- メディア管理:Cloudinary
- 型安全ツール:Zod
- 認証ライブラリ:NextAuth
私が主に担当した認証に関してはこちらの記事にて解説しております。
今回は、「導入しやすさ」と「快適さ」を目標として追求しました。
そのために、Next.jsを用いてさまざまな工夫を施しました。
全部書くと多くなってしまうので、以下の3つに絞って書いていきます。
レンダリング速度の向上
まずは、SSR(Server Side Rendering)とCSR(Client Side Rendering)についてです。
こちらのページが非常にわかりやすく解説されていますが、簡単に言うと、サーバー側でページを用意するのか、それともクライアント側でページを生成するのかによって、表示速度やユーザー体験が大きく変わります。(むっちゃ簡単にいうと)
トップページなど、なるべくSSRを多用して、初回読み込み速度を向上させました。
また、データベースにユーザー情報でリクエストを送る際も、コンポーネントに分けて、なるべくサーバーサイドで取得するようにしました。
import ShopHome from '@/components/shop/ShopHome';
import { getAuthSession } from '@/lib/nextauth';
import { redirect } from 'next/navigation';
export default async function ShopDetail({ params }: { params: { id: string } }) {
//セッションを取得
const session = await getAuthSession();
if (!session || session.role !== 'shop') {
redirect('/login');
}
//遷移したルートとセッションが合うかを確認
const loggedInUserId = session.id;
const shopId = params.id;
if (loggedInUserId !== shopId) {
redirect('/403');
}
return <ShopHome user={session} />;
}
今回のアプリはユーザーに触ってもらう箇所が多く、どうしてもCSRを回避できないことが多かったです。
なので、改善策としては、Suspense
でCSR部分の大きな読み込み箇所を非同期で取得し、その間にSkelton
として読み込み画面を表示させ、他の表示できる部分はすぐ出せるようにしました。
以下が例になります。
データが読み込まれている間、Suspense
は fallback
プロパティとして指定された <Skeleton />
を表示します。
読み込みが完了すると(この例では2秒後)、 元のンポーネントがデータとともにレンダリングされ、スケルトンUIが置き換えられます。
以下では重いグラフを読み込み終わる前に、ダッシュボードと月間売り上げをすぐに出現させています。
直感的な動作で使いやすく
メニューの登録や、注文、イベントを閲覧するとき、スライドバーで選択できたり、なるべく簡単に選択できるように心がけました。
また、やり方がわからない方のためにインフォメーションのページを作り、いつでも左上のi
のマークをクリックすることで閲覧できるようになっています。
人為的なミスを回避
人為的なミスは誰にでもあります。問題は事前にどうやって防ぐかになります。
注文した時に渡すカードを間違えて渡してしまうこともあるかもしれません。
そうした場合、準備のカードが2枚存在してしまう状態になってしまいます。
なので、準備中のものは出せないようにしました。
仕組みとしては、カード自体にstatusを振ります(それが準備中か、準備完了か、引き渡し完了か)。
そして、読み込み時に準備中のものを読み込み、入力時にそれを現在使っているかを出します。
const unreadyCardNumbers = unreadyOrders.map((order) => order.card_number);
const handleCardNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newCardNumber = e.target.value;
setCardNumber(newCardNumber);
if (unreadyCardNumbers.includes(newCardNumber)) {
setCardError('このカードは現在使用中です。');
} else {
setCardError(null);
}
また、注文時などの何かを送信するときはzodでヴァリデーションを行うことで、正常なデータになるようにヴァリデーションするようにしました。
zodは、送信した後に出るのではなく、クライアント側でチェックするので、UXの向上にもつながりました。
最後に
長文にもかかわらず、最後までお読みいただき、本当にありがとうございました。
私はまだエンジニアとしては駆け出しではありますが、日々の出会いや経験が自分を大きく成長させてくれていると実感しています。毎日が新しい発見と学びの連続で、とても充実しています。
今回のハッカソンは初出場で、チームプロジェクトも初めての経験でしたが、多くの実践的なスキルや知識を得ることができました。この貴重な経験を糧に、これからも積極的に挑戦を続けていきたいと思っています。
引き続き応援いただけると幸いです。ありがとうございました!