WORKS
WEB

QR ORDER / STAFF 飲食店QRオーダー|スタッフUI

  • Java 21
  • Spring Boot 3.3.5
  • Spring Data JPA
  • SSE
  • H2 / PostgreSQL
  • Flyway
  • JUnit 5
  • GitHub Actions
  • Docker

制作時間:約6週間(2026年8月〜・個人開発 / Java学習を兼ねた実案件)

ログイン不要で開けます。 デモのデータはすべて架空です。実店舗の営業データとは別のデータベースで動いているため、 画面に出る売上や注文は実在の店の数字ではありません。 店舗側は見学モードのため表示のみで、保存や削除はできません。 お客さまが触るスマホ側は別ページにまとめてあります。

厨房ボード。左の未調理レーンに伝票ごとの品が並び、右の調理済みレーンに卓ごとの品が並ぶ。10分を過ぎた伝票は帯が薄赤になる

OVERVIEW

プロジェクト概要

課題

学芸大学の鉄板料理居酒屋で実際に使うことが前提のシステムです。 お客さまが席のQRを読み、自分のスマホから注文します。それが同じ瞬間に厨房のボードへ届き、 状態を進めるとお客さまの伝票にも反映される——この流れが途切れないことが最低条件でした。 さらに会計にはテーブルチャージ・深夜料金・消費税が絡みます。

目的

注文の一場面だけを便利にしても足りません。 目的は来店から会計、その先の帳簿までをひと続きにすることです。 卓のQRから入った注文がそのまま厨房へ流れ、会計の金額になり、売上と原価の記録に積み上がる。 途中が紙や口頭で途切れると、そこに人が突き合わせる仕事が残ります。 Javaの学習を兼ねた実案件です。設計・実装・テスト・公開までを一人で通しました。

BUSINESS MODEL

売り方を先に決めて、機能を逆算した

QRオーダーは比較記事に10社以上が並ぶ市場です。 リクルート(Airレジ オーダー)、カカクコム(食べログオーダー)、Square、STORES のように 別の本業から来た資本も入っています。 注文と会計だけを作れば、同じものがもう1つ増えるだけになります。そこで先に売り方を決めました。

売り先は店ではなく顧問税理士。出向していた会社で実際に回っていたモデルで、 税理士を100社に絞り、その税理士を通じて店のメニューやPOPのデザインを売っていました。 ここはまだ構想の段階です。

売り先が決まると、作るものが決まります。 税理士が使うなら注文と会計の先が要る——レシートの読み取り、原価、消費税の集計、仕訳の書き出し。 店舗側33画面のうち11枚がそこです(仕入れ・原価6枚/税理士5枚)。 他社が注文と会計で止まっているところが、そのまま違いになりました。

最終確認は税理士が行う前提なので、読み取った結果はそのまま保存しません。

相手 メリット
税理士 「うちに頼めば入力が要りません」と言える。AIで職域が狭まるなかで価格競争以外の売りになる
人件費が浮く。入力に人を使っていた作業そのものが無くなる
顧問先が離れにくい。一度入れた店は税理士を替えるとQR注文が使えなくなる
店主 人手不足の穴を埋められる。お客さまが自分のスマホから注文するので注文を取る人が要らない
経営状況をぱっと判断できる。FL6割・賃料1割という型と今月の実績を並べて、差だけを赤で出す
PCが苦手でも入力できる。レシートは写真を撮って送るだけで、スマホならカメラが直接開く
仕訳の書き出しまで進む。撮ったあとの読み取りも集計も同じアプリの中で終わる
選定も契約も要らない。顧問税理士が持ってくる
私 1件で複数の店に届く。税理士は飲食店を何件も抱えている
1軒ずつ回らなくていい。売り先が税理士なので店への営業がいらない

PROCESS

制作プロセス

  1. RULE

    テーブルチャージ・深夜料金・消費税の扱いを店主に確認しながら固める。ここが会計の正しさを決める。

  2. BUILD

    Java 21 + Spring Boot + Spring Data JPA で、注文・厨房・会計をひとつのアプリとして実装。

  3. REALTIME

    厨房とホールの盤面はSSEでつなぎ、リロードなしで動く。お客さまの伝票は台数が読めないので軽いポーリング。

  4. TEST

    JUnit 5 + AssertJ + MockMvc で1,516件。金額の計算はここで固定して、触るたびに検証する。

  5. SHIP

    Docker のマルチステージビルドで固め、GitHub Actions と Dependabot の自動マージで運用する。

STAFF UI

スタッフUI(厨房・ホール)

お店側は厨房のタブレットとホールの端末で開きます。厨房ボードはこのページの先頭にある画面です。 画像を押すと拡大できます。

GUEST UI

お客さまUI(スマホ)

席のQRを読むと、お客さまのスマホでこの3画面だけが動きます。 押すのはお客さま自身なので、迷う場所そのものに説明を置く方針で作っています。

RETROSPECTIVE

振り返り・工夫した点

UI / UX

同じアプリを、2つの場所で触ります。 営業中は立ったままiPad(厨房・ホール・品切れ)、 閉店後は座ってPC(仕入れ・食材・レシピ)。 急ぐか急がないかが逆なので、優先することも逆にしました。

2つを作り分けたわけではなく、画面の幅で切り替わります。 iPad Proの横幅(1366px)が収まる線を境に、左のメニューを文字なしのアイコンだけに畳む。 そのぶん本文が712 → 912pxに広がるので、 厨房のように情報を詰めたい画面ほど得をします。

営業中

BUILT FOR A WET FINGER

濡れた手で急いで押す前提にして、厨房ボードを作り直した

押せる部分は48px角を下回らないよう変数で揃え、入力欄の文字は16pxを保ちます。 下回るとiOSが画面を勝手に拡大するからです。 押し間違えも前提にして、調理済みの札は並びを固定し (指を伸ばしている最中に動くと外します)、戻すためのボタンを置きました。

厨房ボードは、この前提で2代目です。もとは受付 → 調理中 → 提供待ちの3レーンで、 並べる単位は注文でした。焼かない瓶ビールまで「調理中」を通り、 4品のうち1品だけ焼き上がっても置き場がありません。 「調理」と「提供」という別々の軸を1本に混ぜていたのが原因で、 軸を調理に絞って2レーンにし、単位も品へ変えました。 1本あたりの幅も広がり、品名を大きくできます。

A PAGE, NOT A MODAL

1枚に3つの仕事が載るので、モーダルをやめてページにした

伝票の画面には、性質の違う3つが載ります。明細を読む、 品を取り消す、会計へ進む。 近い作業に見えて、押したあとに起きることも、戻し方も別です。 1枚のモーダルに入れるといまどれをしているのか分からなくなるので、ページにしました。

引き出しに残したのは払い方だけです。明細が上に見えたまま選べます。 中央のモーダルだと暗幕で全部隠れます。 金額を動かすもの(深夜料金・人数・チャージ免除)も引き出しには入れていません。 変えた金額が暗幕の向こうになると確かめられないからです。

営業時間外

YOU CANNOT GET IT WRONG

手順を覚えなくても、間違えない形にした

商品やカテゴリを足すのは店主です。月に何度かの作業なので、手順は覚えていません。 だから覚えてもらうのではなく、間違えようがない形に寄せました。 大分類は打ち込ませず選ばせます——打ち間違えるとメニューのタブが割れるからです。

公開の前には関門を置きました。必須が埋まるまで「掲載する」は押せません。 ただし止めるだけだと入力の途中で逃げ場がないので、 「下書きのまま保存」はいつでも押せるようにしてあります。 仕込みの合間に半分だけ入れて、あとで続けられます。

NOTHING YOU DIDN'T COME FOR

使う人に、いま要らないものを読ませない

営業中は1画面に詰めますが、ここは逆に粗くしました。 急いでいない代わりに、触るのは月に何度か。 速さより、迷わせないことを優先します。

カテゴリは「足す」と「直す」が1枚にありました。1つ足しに来ただけで、 下に15カテゴリぶんの入力欄が並びます。 画面を割って、開いた瞬間に用事が1つだけ映るようにしました。卓と商品も同じ形です。

バックエンド

NO LOGIN SCREEN FOR STRANGERS

許可した端末以外には、ログイン画面すら出さない

厨房・ホール・管理・税理士の画面は、接続元のIPで閉じています。 許可するのは端末を名指しした一覧(厨房のタブレット、ホールのタブレット…)なので、 店のWi-Fiに繋いだお客さまのスマホも弾かれます。 効かせる位置はログインより前です。 /login も対象に入れてあるので、許可外の端末からは パスワードを試す入口ごと消えます。

絶対の壁ではありません。同じLANにいればIPは偽装できます。 パスワードと独立して破る関門が1つ増える、という位置づけです。 運用の罠もあります。厨房とホールは店内ですが、税理士は事務所から見ます。 一覧に足し忘れると、税理士だけが理由の分からない拒否に遭います。 値を書き間違えたときは起動時に落とすようにしました。 黙って通すと「制限しているつもりでザル」になるからです。

309 GREEN, STILL BROKEN

テストが全部通ったのに、本番では起動しなかった

設定項目を増やしたとき、当時あった309件のテストはすべて通ったのに本番のデータベースでは起動しませんでした。 Hibernate の ddl-auto は、すでに行が入っているテーブルに NOT NULL の列を足せません。 既存の行に何を入れるか決められないからです。 テストは毎回まっさらなDBを作るので「無いテーブルを作る」経路しか通らず、 この失敗は原理的に検出できませんでした。 以後、スキーマを変えたときは本番と同じ状態のDBで一度起動して確かめています。

THE BUG THAT THROWS NOTHING

例外もエラーも出ないまま、厨房ボードが空になった

公開後、デモの厨房ボードが空になりました。例外もエラーも出ていません。 ログを時刻順に読んで、注文を作る処理が卓を作る処理より先に走っていたと分かりました。 Spring は起動時処理が複数あるとき実行順を保証せず、順序を書かなければクラスパスを走査した順で決まります。 ローカルとコンテナで、その順序が逆になっていました。 厄介なのは、逆順でも「対象が見つからないので0件作った」でコードとしては正常に終わることです。 画面は開きます。ただ中身がありません。

ONE SOURCE OF TRUTH

注文の状態を、品の段階から導いて書き戻す

DBには品ごとの段階(LineStage:未調理 → 調理済み → 提供済み)を足しました。 マイグレーションは Flyway の V20 で、テーブルは増やさず order_line に3列足すだけです。既存の行は、注文の状態から段階へ読み替えて埋めています。

注文の状態は品の段階から導いて書き戻します。人が両方を動かせると必ず食い違うからです。 ここで1つ引っかかりました。品を「← 戻す」で未調理に戻せるということは、注文の側では 提供待ちから調理中へ戻るという後ろ向きの遷移が起きるということです。 注文側の遷移表は前向きしか許していないので、導いた状態はその検査を通さずに書き込む形にしました。 両方で検査すると「画面では戻せたのに保存で落ちる」が起きます。

THE RECEIPT NEVER CHANGES

伝票は、あとから何を直しても当時のまま残る

明細には商品名・価格・税率を注文した時点の値でコピーします。マスタは参照しません。 商品を消しても、値上げしても、税率が変わっても、去年の伝票は当時の金額のままです。 参照にしておくと、過去の売上が今日の設定で書き換わります。

日付はカレンダーの日ではなく営業日で持ちます。切り替え時刻は店舗設定にあるので、 深夜2時の注文も前日の売上に入ります。注文番号は営業日ごとに1から振り直し、 同じ日に同じ番号は作れないことを一意制約で守ります。 インデックスは画面の引き方に合わせました——厨房ボードは状態、注文履歴は営業日で引くからです。

LIVE

公開デモ(ログイン不要)

ボタンを押すだけで入れます。ID もパスワードも要りません (店舗側は最初に、見学モードの説明が1枚はさまります)。 これは公開デモ版で、データはすべて架空です。実店舗の営業データとは別のデータベースで動いています。 店舗側は見学モードのため表示のみで、保存や削除はできません(厨房ボードで品の状態を進める操作だけ触れます)。 無料枠のため、時間帯によっては最初の1回だけ起動に30〜40秒かかります。

お客さま側の入口はお客さまUI のページにあります。 2 つを並べて開くと、注文が厨房に届くところまで確かめられます。

  1. OPEN

    「店舗管理画面」を開き、「厨房ボードへ進む」を押す。

  2. ORDER

    別のタブでスマホ注文画面を開き、メニューから注文する。

  3. WATCH

    厨房ボードに、リロードなしで注文が届く。

  4. BACK

    厨房ボードで品を「調理済み」に進めると、スマホ側の表示も変わる。

コピーしました