はじめに

「ポートフォリオに業務システムを作りたいけど、何から始めればいいの?」

Todoアプリやブログはひと通り作ったけれど、次はもう少し「仕事っぽい」ものを作ってみたい。
そう思ったときに、いきなりコードを書き始めてしまうと、途中で「あれ、これ何を作るんだっけ?」となりがちですよね。

私はいま、ポートフォリオとして社内業務システム「office-hub」を個人開発しています。
社員がポータル画面にログインして、そこから自分が使えるシステムへ移動する構成で、その第1弾として備品在庫管理システムを作っています。

この連載では、開発の工程を1フェーズ=1記事で記録していきます。記事の番号は開発のフェーズの番号とそろえていて、#0のこの記事は、コードを1行も書く前の「設計」のフェーズです。

  • 題材をどう選んだか(4つの候補を比べた理由)
  • 要件定義書に何を書いたか
  • 「未決事項」を「決定事項」に書き換えて残すやり方
  • 設計書の段階で見つかったバグ4つ

を順番に紹介します。

先にお伝えしておくと、設計書はAI(Claude)と対話しながら一緒に作りました。
この記事でも、AIに助けてもらったところはそのまま書いていきます。

office-hubの全体像

まずは、何を作っているのかをざっくり紹介します。

領域使っている技術
バックエンドPHP / Laravel
フロントエンドReact / TypeScript / Vite / Tailwind CSS
データベースMySQL
認証Laravel Sanctum
開発環境Laravel Sail(Docker)

開発は次のように段階(マイルストーン)を分けて進めています。
この記事を書いている時点では、M0(環境構築・ログイン・ポータル画面)まで完成しています。

段階内容
M0環境構築、ログイン、ポータル画面
M1備品マスタ、在庫の入出庫
M2貸出・返却
M3棚卸し
M4アラート、ダッシュボード、監査ログ
M5CSV、QRコード、テストの拡充

コードは GitHub(riu-414/office-hub) で公開しています。
この記事で紹介する設計書も docs フォルダに入っています。

題材選び:4つの候補を比べた

「実務で使えそうな業務システム」という方針で、最初に4つの候補を出しました。
比べたのは技術的な見せ場と弱点です。

候補技術的な見せ場弱点
備品在庫管理+貸出在庫の増減を正しく保つ処理、履歴からの在庫数の計算、定期的な通知題材が定番で埋もれやすい
会議室・設備予約二重予約の防止、カレンダー画面データの構造が小さく、見た目勝負になりがち
受発注・請求管理状態の移り変わりの設計、金額計算、PDF出力業務の知識が必要で、要件がふくらみやすい
ヘルプデスク通知、対応期限切れの自動判定、全文検索登録・一覧・コメントだけで終わると平凡になる

選んだのは備品在庫管理です。

理由は、「定番だからこそ、作りの丁寧さで差が出る」と考えたからです。
同じ題材の作品が多いということは、見る人にとって比べやすいということでもあります。
そこで、次のような「一段深い」部分を入れることにしました。

  • 在庫の数をそのまま保存せず、入出庫の履歴から計算できるようにする
  • 2人が同時に在庫を操作しても数がズレないようにする
  • 棚卸し(システム上の数と、実際に数えた数の差を調整する機能)
  • 操作の記録(監査ログ)と、役割ごとの権限

このあたりは実装する段階(M1以降)で、それぞれ記事にする予定です。

要件定義書に書いたこと

題材が決まったら、次は要件定義書です。
「このシステムで何をできるようにするのか」を文章にまとめたもので、今回はこんな章立てにしました。

  1. 背景と目的
  2. スコープ(やること・やらないこと)
  3. 用語の定義
  4. 利用者と役割(ロール)
  5. 機能要件(41件)
  6. 主な業務の流れ
  7. データの構造(ER図)
  8. 画面一覧(17画面)
  9. 非機能要件(速さ・安全性など)
  10. 設計方針
  11. リリース計画
  12. 決定事項

全部を紹介すると長くなるので、作ってみて「これはやってよかった」と感じたポイントを3つに絞ってお話しします。

ポイント1:目的を「在庫を管理する」にしなかった

要件定義書の目的には、こう書いています。

在庫の「現在の数」と「そうなった経緯」を、関係者全員が同じ画面で確認できる状態にする

「在庫を管理する」だけだと、在庫の数さえ合っていればいいことになってしまいます。でも実際の現場で困るのは、「帳簿と実物の数が合わないけど、いつズレたのか分からない」ことなんですよね。

目的に「そうなった経緯」まで入れたことで、このあとの設計の判断がぶれなくなりました。たとえば「在庫の履歴は消さない」「間違えた記録も、取り消しの記録を追加して残す」といった方針は、すべてこの一文から出てきています。

ポイント2:「やらないこと」を理由つきで書いた

スコープの章では、やることだけでなくやらないことも書きました。たとえばこんな感じです。

やらないこと理由
発注業務そのもの「在庫が減ったら知らせる」までを担当にする。発注は会社の既存の流れに任せる
会計との連携会計の要件は重く、「在庫を見える化する」という目的から外れる
スマホアプリスマホ・タブレットでも見やすいWeb画面で代わりにする

個人開発だと「あれもこれも」と機能を足したくなりますが、やらないことを先に決めておくと、途中で迷ったときの判断が楽になります。

機能要件はMoSCoW(モスクワ)法で優先度をつけました。

記号意味今回の件数
Mustこれがないとリリースできない28件
Should重要だが、なくてもリリースはできる8件
Couldあればうれしい5件
Won’t今回はやらない(スコープ外の表で管理)

「重要度:高・中・低」よりも判断がはっきりするのがいいところです。
Mustの機能を全部合わせたものが、そのままリリースの範囲になります。

ポイント3:「未決事項」を「決定事項」に書き換えて残した

最初の版では、決めきれない論点を未決事項として6件書き出し、それぞれに選択肢を並べました。
そのうえで1つずつ結論を出していきました。

論点決めたこと
貸し出す備品を1台ずつ管理するか数量だけで管理する
貸出に申請・承認の流れを入れるか入れない
在庫の履歴を一般の社員にも見せるか見せる
保管場所を最初から複数扱うか備品ごとに1か所
在庫が減ったときの通知先管理者と在庫担当者の全員
画像の保存先サーバー内(設定でクラウドにも切り替えられる形)

大事なのは、決めたあとに結論だけでなく「なぜそう決めたか」も残したことです。

たとえば「履歴を一般の社員にも見せる」は、目的の「関係者全員が同じ画面で確認できる」にそのまま沿っています。見せないことにすると、目的と矛盾してしまうんですね。

理由まで書いておくと、こんないいことがあります。

  • 半年後の自分が「なんでこうなってるんだっけ?」と同じ議論をくり返さずに済む
  • 前提が変わったときに「この理由が崩れたから見直そう」と判断できる
  • ポートフォリオを見た人に、考えた過程まで伝えられる

設計書の段階で見つかったバグ4つ

要件定義書のほかに、DB設計書(データベースのテーブルの設計)とAPI設計書(画面とサーバーのやり取りの設計)も作りました。

そして、この3つの設計書をAIにレビューしてもらったところ、バグが4つ見つかりました。
まだコードを1行も書いていないのに、です。
どれも、設計書を書いている最中には見落としていたものです。

バグ1:在庫が少ないかどうかの判定式

備品には「この数を下回ったら知らせる」という発注点を設定できるようにしています。
発注点を0にした備品は「知らせなくていい備品」という扱いです。

最初は、在庫が少ないかどうかをこう判定していました。

on_hand_quantity <= reorder_point
-- 手元の在庫数 <= 発注点

一見よさそうですが、発注点が0の備品の在庫が0になると、0 <= 0 で「在庫が少ない」と判定されてしまいます。
知らせなくていいはずの備品なのに、です。

修正後はこうなりました。

reorder_point > 0 AND on_hand_quantity <= reorder_point

「0のときどうなる?」という境界の値を考えていなかった、典型的なミスでした。

バグ2:書いてあるのに、テーブルがない

API設計書には「同じ登録が2回届いても1回分として扱うために、受け取った目印(キー)を24時間保存する」と書いていました。
ところが、その保存先のテーブルがDB設計書のどこにもありませんでした。

設計書を3つに分けて書いたせいで、片方にしか存在しない仕様が生まれていたんです。
設計書を分けるなら、「この仕様は別の設計書のここに対応する」と、お互いの参照先を書いておく必要があると学びました。

バグ3:「なければ作る」が同時アクセスに弱い

在庫の数を更新する処理に、「在庫の行がまだなければ作ってから更新する」と書いていました。

これは、2人が同時に初めての操作をすると、2人とも「まだない」と判断して、同じ行を2つ作ろうとしてしまう危険があります。
エラーになるか、最悪の場合は在庫が二重に数えられます。

修正では、備品を登録するときに、在庫の行も同時に作ることにしました。
在庫を更新するときには「必ずある」前提で読むだけなので、取り合いが起きません。

バグ4:UNIQUE制約がNULLのときに効かない

ユーザーを削除するときは、データを本当に消すのではなく「削除した日時」を記録する論理削除にしています。
そして、削除した人と同じメールアドレスで再登録できるように、「メールアドレス」と「削除日時」の組み合わせが重複しないようにする制約(UNIQUE制約)を考えていました。

ところが、MySQLのUNIQUE制約はNULL(値がない状態)同士を「別の値」として扱います。
削除されていないユーザーは削除日時がNULLなので、同じメールアドレスの人を何人でも登録できてしまうんです。

これは実際にデータベースを作る直前に分かったもので、そのまま作っていたら確実にハマっていました。
この話はかなり奥が深いので、データベースを作る回でくわしく書きます。

4つのバグから学んだこと

振り返ると、バグ2・3・4には共通点がありました。

  • 「別の場所に書いたつもり」(バグ2)
  • 「なければ作る」「重複していなければ登録する」を、確認してから実行する(バグ3・4)

特に後者は「確認してから実行する」の間に、別の人の操作が割り込める、という同じ構図です。
この反省から、重複を防ぐのはプログラムの確認ではなく、データベースの制約に任せるという方針を設計全体で採るようになりました。

そしてもう1つ。設計書もコードと同じで、レビューが必要だということです。
今回は人ではなくAIのレビューでしたが、コードを書く前にこれだけ見つかったのは大きかったです。

まとめ

  • 題材は「定番だからこそ作りの丁寧さで差が出る」と考えて、備品在庫管理を選んだ
  • 要件定義書の目的は「在庫を管理する」ではなく、「今の数」と「そうなった経緯」を全員で確認できることにした。これがあとの設計判断の軸になった
  • 「やらないこと」も理由つきで書き、機能はMoSCoW法で優先度をつけた
  • 未決事項は、結論と理由をセットで「決定事項」に残した
  • 設計書をAIにレビューしてもらったら、コードを書く前にバグが4つ見つかった

次回は、Laravel Sail を使った開発環境の構築について書く予定です。

私はふだん製造業で働きながら、WordPressを中心にWebサイトの制作をしています。
これまでの制作実績はポートフォリオにまとめています。
office-hubのコードはGitHubで公開しているので、設計書の中身が気になった方はのぞいてみてください。