KURA — 要件定義書 兼 Claude Code 実装指示書
版数 2.0(RELATE 準拠構成 / DBレス / 記事埋め込み型デモ)/ 作成日 2026-08-24 / 発行 ソウゾウ合同会社 用途 SEO記事「【デモで試せる】Claude Codeで在庫管理システムを自作する方法と費用|動画つき資料【2026年】」に組み込む実演デモ
このドキュメントの使い方(Claude Code へ)
これ1枚が、規約・仕様・タスクのすべて。リポジトリ直下に CLAUDE.md として置き、毎回読み込むこと。
- 実装は「12. 実装タスク」の順に進める。フェーズを飛ばさない。
- 各タスクの括弧内は要件ID。着手前に該当章を読むこと。
- 仕様がここに書かれていない場合は、推測で実装せず質問する。
- スコープ外(3章 Won't have)は、思いついても実装しない。
- 「デモだから」を理由に品質を落とす判断はしない。
姉妹プロジェクトとの関係 HIREBASE(求人)/ RELATE(顧客管理)/ CASTA(動画配信)/ FIELDPIN(位置情報)/ KANADE(音楽)/ MIWAKE(画像認識)/ KAMIWAZA(美容室予約)に続くシリーズ。並べて「同じテンプレ」と思われた時点で失敗。 **特に RELATE との差別化に注意。**どちらも「白背景・高密度・業務システム」だが、 RELATE=罫線と等幅数字(Roboto Mono)で表を読ませる/藍 KURA=表の各行に在庫の水位バーを描く/ティール+アンバー、数値は縦長(Roboto Condensed) 8章のデザイン要件は意図的に別方向に振っている。トークンを流用しないこと。
目次
- プロジェクト概要
- アーキテクチャ方針
- スコープ定義
- ロールとデモ切替
- 画面一覧とユーザーフロー
- 機能要件
- データ設計
- デザイン要件
- 技術要件・ディレクトリ構成
- 非機能要件
- 費用設計
- 実装タスク
- 受入基準
1. プロジェクト概要
背景と目的
「在庫管理システムを作りたい」という相談の背景は、ほぼ次のいずれか。
- Excelで在庫表を運用しているが、更新が追いつかず、誰も数字を信じていない
- 在庫はあるのに欠品する。倉庫にはあるのに売れない
- 棚卸のたびに大きな差異が出て、原因が追えない
この領域には、最初に共有すべき事実が2つある。
事実①:システムを入れても、理論在庫と実在庫は必ずズレる
記録漏れ、破損、誤出荷、紛失、返品の処理漏れ——ズレは必ず生まれる。「システムを入れれば在庫が合う」は嘘。
正しい目標は「ズレをゼロにする」ではなく、**「ズレを早く見つけて、原因を特定して、直せるようにする」**こと。だから在庫管理システムの本体は、在庫数を表示する画面ではなく、入出庫の履歴と棚卸の仕組みにある。
事実②:「在庫数」は1つではない。4つある
| 種類 | 意味 | これを混同すると |
|---|---|---|
| 実在庫 | いま物理的に棚にある数 | — |
| 引当済 | 受注済みで、出荷を待っている数 | 棚にあるのに売ってしまい、欠品する |
| 有効在庫 | 実在庫 − 引当済(=これから売れる数) | — |
| 入荷予定 | 発注済みで、まだ届いていない数 | すでに発注済みなのに重複発注し、過剰在庫になる |
「欠品と過剰在庫が同時に起きている」会社は、ほぼこの4つを分けて持っていない。
KURA はこの2つの事実を前提に設計する。
| 目的 | 内容 |
|---|---|
| 実装力の証明 | 入出庫・引当・棚卸・ロット期限・発注点・拠点間移動まで、実運用に耐える在庫エンジンを見せる |
| 期待値の正常化 | **「在庫は必ずズレる」「在庫数は4つある」**を、実物で示す。これが最大の価値 |
| 操作効率の設計力の証明 | バーコード連続スキャン、一括操作、キーボード完結。毎日触る人の速度を設計できることを示す |
| 費用判断の材料 | ハードウェア(ハンディ端末・ラベルプリンタ)とマスタ整備工数を含めた総額を示す |
プロダクト定義
| 項目 | 内容 |
|---|---|
| プロダクト名 | KURA(蔵)※仮称 |
| 一言定義 | 入出庫の履歴から在庫を組み立てる、中小企業のための在庫管理システム |
| 想定業態 | 卸・小売、EC、食品・飲料(ロット・期限あり)、部品・資材、製造(原材料と製品) |
| 提供形態 | レスポンシブWebアプリ(PWA)。フロントエンド完結(サーバー側の永続化なし) |
| 主戦場 | 現場はスマホ/タブレット(棚の前で入出庫を打つ)。事務所はPC(分析・発注・マスタ) |
| 想定利用者 | 記事の読者。登録なしで、現場と管理の両方を体験できる |
プロダクトコンセプト
「在庫数を書き換えない。出来事を記録する。」
KURA は在庫数を直接更新しない。入庫・出庫・移動・調整という出来事(トランザクション)を積み上げ、その合計として在庫を算出する。だから「なぜこの数になったか」が常に遡れる。差異が出たとき、原因に辿り着ける。
デモとしての成功条件
- 4つの在庫数が常に見える — 実在庫・引当済・有効在庫・入荷予定が、同じ画面に並んでいる
- 在庫が「積み上がる」ことが分かる — 履歴を開くと、現在庫がどの取引の合計かが追える
- 差異が体験できる — 棚卸で数を入れると差異が出て、原因を分類し、承認して初めて在庫が動く
- ロールで見え方が変わる — 現場担当は自拠点だけ、原価は在庫管理者以上のみ
- 速い — バーコードを連続で打っても止まらない
- リセットできる — 誰が触った後でも初期状態に戻せる
2. アーキテクチャ方針
基本方針
バックエンドとデータベースを持たない。すべてブラウザ内で完結させる。
| 一般的な構成 | 本プロジェクト |
|---|---|
| PostgreSQL(トランザクション・一意制約) | シードデータ + ブラウザ内ストア |
| 認証・SSO | デモ用ロール切替(ワンクリック) |
| REST / GraphQL API | ストア上の同期的な操作 |
| バーコード読取(ハンディ端末) | キー入力(既定)+ カメラスキャン(任意) |
| ラベル印刷 | 印刷レイアウトの表示とPDF生成まで |
| EC・受発注・会計システム連携 | CSVインポート/エクスポートで代替 |
| 在庫の算出 | これは本物。 トランザクションの積み上げで計算する |
なぜこの構成にするか
- LPデモとして最適 — 訪問者は登録もログインもせずに、全機能を即座に触れる
- 自社の商品マスタをCSVで投入して試せる — データが外に出ないため、営業上かなり強い
- 開発速度 — 認証・APIの実装が不要になり、在庫ロジックとUIの作り込みに全時間を投下できる
- 運用コストゼロ — Vercel の静的配信のみ
在庫算出の原則(最重要の設計判断)
在庫数を保持しない。トランザクションから常に算出する。
在庫スナップショット = Σ トランザクション
入庫 (+)
出庫 (−)
拠点間移動 (出荷元 −、到着先 +、その間は「移動中」)
棚間移動 (棚Aから −、棚Bへ +)
在庫調整 (± 理由コード付き)
棚卸差異 (± 承認済みのみ)
期首棚卸 (+ 初期在庫の投入)
| ID | 規約 |
|---|---|
| INV-01 | 在庫数量をストアに保存しないこと。 lib/inventory/ で常にトランザクションから算出する |
| INV-02 | 在庫を変更する唯一の方法は、トランザクションを追加すること。 数量を直接書き換えるコードを書かない |
| INV-03 | トランザクションは削除・編集しない。 取消は「逆仕訳」の追加で行う(履歴が消えないようにする) |
| INV-04 | 算出結果はメモ化してよいが、**メモ化のキーはトランザクションの世代番号(seq)**とし、追加時に必ず無効化すること |
| INV-05 | lib/inventory/ は純粋関数のみ。ストア・DOM に依存させず、現在時刻は引数で受け取ること |
| INV-06 | 4つの在庫数(実在庫・引当済・有効在庫・入荷予定)をすべて同じ関数から返すこと。 別々に計算しない |
この設計が、記事の技術パートの中核になる。 「在庫数を UPDATE する」設計との違いを説明できることが専門性になる。
差し替え可能性の担保
データアクセスは必ず lib/repo/ のリポジトリ層を経由すること。 コンポーネントからストアを直接触らない。
[ コンポーネント ]
↓ 呼ぶのはこの層だけ
[ lib/repo/*.ts ] ← 全メソッドを async にしておく
↓
[ lib/inventory/ ] ← 在庫算出(純粋関数)
[ lib/store/*.ts ] ← Zustand + persist(localStorage)
↓
[ lib/seed/*.ts ] ← 初期データ
リポジトリの各メソッドは、中身が同期処理でも async で定義し、await で呼ぶ。実案件へ転用する際、リポジトリの実装だけをAPI呼び出しに差し替えれば、UI層は一行も変更せずに済む。
記事への組み込み
| 段 | 配置 | 中身 |
|---|---|---|
| ① 記事内インライン | 記事の冒頭〜中盤、<iframe> |
1商品の在庫水位バー + 入出庫パネル。 入庫・出庫・引当を打つと4つの数字とバーが動く |
| ② 全画面デモ | 「全画面で試す」カード → 別タブ | アプリ全体(棚卸・ロット期限・発注点・拠点移動・分析) |
記事内で見せるのは「4つの在庫数が別々に動くこと」だけに絞る。 これが1章の事実②そのものであり、スクロール中に一目で伝わる唯一の絵になる。
| ID | 要件 |
|---|---|
| EMB-01 | 記事内 iframe は loading="lazy" とし、ビューポート接近まで読み込まないこと |
| EMB-02 | 埋め込みモードのJS初期バンドルは 60KB以下(gzip後)とすること |
| EMB-03 | 埋め込み iframe が記事の LCP 要素にならないこと |
| EMB-04 | 埋め込みモードでカメラ(getUserMedia)を呼ばないこと。 バーコード入力はキー入力のみとすること |
| EMB-05 | 埋め込みモードでは、ヘッダー・ナビ・ロール切替バーを非表示とし、アプリ内遷移を禁止すること。「全画面で試す」は target="_blank" |
| EMB-06 | GA4 で計測すること:demo_open / txn_posted(入出庫を打った)/allocation_created(引当を打った)/stocktake_done / csv_imported / role_switch / doc_download / form_click / line_click |
| EMB-07 | CSVでインポートされた商品名・取引先名を GA4 に送らないこと |
| EMB-08 | ?embed=1 のURLは noindex とすること。デモ本体は固有の title / description を持つこと |
永続化の範囲
| 対象 | 挙動 |
|---|---|
| シードデータ(商品・拠点・棚・取引先・過去のトランザクション) | 初期投入。マスタは編集可能 |
| 入出庫・移動・調整・棚卸・発注・引当 | localStorage に保存。リロードしても残る |
| CSVインポートしたデータ | localStorage に保存される |
| 添付ファイル(検品写真等) | セッション内のみ(Object URL) |
| リセット | 「デモをリセット」で localStorage をクリア |
擬似ディレイ は 150〜400ms。ただし在庫の再算出と一覧の絞り込みは 100ms以下とし、ディレイを入れない(業務システムで待たされる感覚は品質の低さとして受け取られる)。
3. スコープ定義
在庫照会・マスタ
| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-M01 | 商品マスタ | SKU、商品名、JAN、カテゴリ、単位、単位換算、原価、売価、保管条件 | Must |
| F-M02 | 在庫一覧 | 4つの在庫数を同時表示。 水位バー、絞り込み、列カスタマイズ、保存ビュー | Must |
| F-M03 | 商品詳細 | 4つの在庫数、拠点別内訳、ロット別内訳、履歴、発注点、回転率 | Must |
| F-M04 | 在庫履歴(元帳) | 全トランザクションの時系列と残高推移。現在庫の積み上げが追える | Must |
| F-M05 | 拠点・ロケーション管理 | 拠点、棚番(エリア-列-段)、保管条件、巡回順 | Must |
| F-M06 | 取引先管理 | 仕入先・出荷先、リードタイム、最小発注金額 | Should |
| F-M07 | CSVインポート | 商品・在庫初期値・取引先。列マッピング、プレビュー、エラー行表示 | Must |
| F-M08 | CSVエクスポート | 現在の絞り込み・表示列で出力 | Must |
| F-M09 | 一括操作 | 複数商品のカテゴリ変更・発注点変更・棚番変更 | Should |
入出庫・移動
| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-T01 | 入庫(検品) | 発注書からの入荷、数量差異、ロット・期限入力、部分入荷、棚入れ | Must |
| F-T02 | 出庫(ピッキング) | 出荷指示からの出庫、棚番順リスト、FEFO推奨、実績入力 | Must |
| F-T03 | 引当 | 受注に対する在庫の確保。実在庫は変えず有効在庫のみ減算。引当解除 | Must |
| F-T04 | バーコードスキャン | キー入力(既定)+ カメラ(任意)。連続スキャンで数量を積み上げ | Must |
| F-T05 | 在庫移動 | 拠点間・棚間。拠点間は「移動中」状態を持つ | Must |
| F-T06 | 在庫調整 | 破損・紛失・期限切れ廃棄・誤記訂正。理由コード必須 | Must |
| F-T07 | 取消(逆仕訳) | 誤登録の取消。削除ではなく逆仕訳を追加 | Must |
| F-T08 | オフライン対応 | 圏外でも入出庫を記録し、復帰時に同期 | Should |
| F-T09 | 検品写真の添付 | 破損時の証跡 | Could |
棚卸
| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-S01 | 棚卸の開始 | 対象範囲(拠点/棚/カテゴリ)を指定。理論在庫を凍結 | Must |
| F-S02 | カウント入力 | 棚番順リスト、バーコード、実数入力、担当者割当、進捗率 | Must |
| F-S03 | 差異一覧 | 理論 vs 実数、差異数・差異金額、差異率ソート | Must |
| F-S04 | 差異の原因分類 | 記録漏れ/破損/紛失/誤出荷/棚違い/不明。分類必須 | Must |
| F-S05 | 承認と確定 | 承認で調整トランザクションを生成。承認前は在庫を動かさない | Must |
| F-S06 | 棚卸履歴 | 過去の棚卸、差異推移、精度の改善度 | Should |
| F-S07 | 循環棚卸 | ABC区分に応じた棚卸頻度の設定と対象提示 | Could |
発注・ロット期限
| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-O01 | 発注点・安全在庫 | 商品ごとに設定。リードタイム × 平均出庫からの推奨値算出も提供 | Must |
| F-O02 | 推奨発注リスト | 判定式「有効在庫 + 入荷予定 < 発注点」。推奨数量、仕入先別グループ | Must |
| F-O03 | 発注書の作成 | 仕入先別にまとめる。PDF出力 | Should |
| F-O04 | 入荷予定の管理 | 発注残、予定日、遅延アラート | Must |
| F-O05 | ロット管理 | ロット番号、入荷日、賞味/使用期限 | Must |
| F-O06 | FEFO出庫 | 期限が近いロットから出す推奨。逸脱時は理由確認 | Must |
| F-O07 | 期限アラート | 期限切れ間近・期限切れの一覧。カテゴリ別の閾値設定 | Must |
| F-O08 | シリアル管理 | 個体識別が必要な商品のシリアル追跡 | Could |
分析・管理
| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-A01 | ダッシュボード | 欠品リスク、期限アラート、要発注、未承認の棚卸差異、在庫金額 | Must |
| F-A02 | 在庫回転率・滞留在庫 | 商品別の回転率、最終出庫からの経過日数、滞留金額 | Must |
| F-A03 | ABC分析 | 出庫金額の累積構成比でA/B/C区分。パレート図 | Should |
| F-A04 | 在庫金額の推移 | 移動平均法での評価額。月次推移、拠点別 | Should |
| F-A05 | 欠品分析 | 欠品発生回数、機会損失の推定 | Should |
| F-A06 | 差異分析 | 原因別・拠点別・担当者別の差異金額 | Should |
| F-A07 | 操作ログ | 誰がいつ何を登録・承認したか | Should |
| F-A08 | ユーザー・権限管理 | メンバー、ロール、担当拠点 | Should |
共通・基盤
| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-C01 | ロール切替 | 現場担当/在庫管理者/管理者 をワンクリック切替 | Must |
| F-C02 | 埋め込みモード | ?embed=1 で在庫水位パネルのみ(2章) |
Must |
| F-C03 | グローバル検索 | SKU・商品名・JAN・ロット番号を横断検索 | Must |
| F-C04 | コマンドパレット | ⌘K / Ctrl+K で検索・遷移・入出庫の起動 |
Should |
| F-C05 | 保存ビュー | 絞り込み + 表示列 + 並び順を保存。個人/共有 | Must |
| F-C06 | 記事への導線 | 元記事へのリンク、資料ダウンロード、問い合わせ | Must |
| F-C07 | デモリセット | localStorage をクリア | Must |
| F-C08 | ガイドツアー | 初回訪問時に「何を試せるか」を3ステップで案内 | Should |
対象外(Won't have)
| 項目 | 理由 |
|---|---|
| データベース・バックエンドAPI | 2章の方針に基づく |
| 本物の認証・SSO | ロール切替で代替 |
| EC・受発注・会計システムとのAPI連携 | CSV入出力で代替。個別案件のオプション |
| 実際のラベル印刷(プリンタ制御) | 印刷レイアウトとPDF生成までに留める |
| ハンディターミナル専用アプリ | Webで対応。ハード比較は11章で扱う |
| WMS的機能(ロケーション最適化、ピッキング経路最適化) | 棚番順の表示までに留める |
| 生産管理・BOM(部品構成)展開・製造振替 | 対象外 |
| 原価計算の高度な手法(総平均・個別法・標準原価) | 移動平均法のみ実装 |
| 需要予測AIによる自動発注 | 発注点+リードタイムのルールベースに限定 |
| セット品(キット)の在庫連動 | 対象外。要件定義段階で洗い出す論点として扱う |
| 多通貨・輸出入業務 | 対象外 |
スコープリスク: 在庫管理は「うちはこうなんです」が最も多い領域。単位換算(ケース/ボール/バラ)・ロット規則・セット品・引当の優先順位は業種で大きく違い、在庫算出の根幹に影響する。後付けは大改修になるため、要件定義の段階で必ず洗い出す。資料PDFの「在庫要件チェックシート」はこの洗い出しのために作る。
4. ロールとデモ切替
ロール設計は、そのまま実案件での権限制御・RLS の設計根拠になる。「誰が・どのデータを・どの項目まで見られるか」を先に確定させる。
ロール定義
| ロールID | 名称 | 見えるデータ | 特徴的な権限 |
|---|---|---|---|
staff |
現場担当 | 担当拠点の在庫のみ。原価・在庫金額は非表示 | 入庫・出庫・移動・棚卸カウント。調整と承認は不可 |
keeper |
在庫管理者 | 全拠点の在庫。原価・在庫金額を閲覧可 | 上記 + 在庫調整・棚卸の承認・発注・マスタ編集・共有ビュー作成 |
admin |
管理者 | 全社 | 上記 + ユーザー権限・拠点設定・操作ログ・全分析 |
権限マトリクス
| 操作 | staff | keeper | admin |
|---|---|---|---|
| 在庫照会(担当拠点) | ○ | ○ | ○ |
| 在庫照会(全拠点) | — | ○ | ○ |
| 原価・在庫金額の閲覧 | — | ○ | ○ |
| 入庫・出庫・棚間移動 | ○ | ○ | ○ |
| 引当の作成・解除 | — | ○ | ○ |
| 拠点間移動の起票 | △自拠点発のみ | ○ | ○ |
| 在庫調整(理由付き) | — | ○ | ○ |
| 取消(逆仕訳) | — | ○ | ○ |
| 棚卸のカウント入力 | ○ | ○ | ○ |
| 棚卸の承認 | — | ○ | ○ |
| 発注の登録・発注書出力 | — | ○ | ○ |
| 商品・発注点マスタの編集 | — | ○ | ○ |
| 共有ビューの作成 | — | ○ | ○ |
| 分析(金額を含む) | — | ○ | ○ |
| ユーザー権限・拠点設定・操作ログ | — | — | ○ |
データスコープと項目スコープ(デモの見せ場)
在庫管理では「見える行」だけでなく「見える列」も権限で変わる。 現場担当に原価を見せない運用は一般的。
- 現場担当(東京倉庫)で在庫一覧 → 東京倉庫の 240 SKU。原価列と在庫金額列がそもそも存在しない
- 在庫管理者に切替 → 全拠点 620 SKU。原価・在庫金額・粗利が出現する
- 管理者に切替 → 設定メニューと操作ログが出現する
| ID | 要件 |
|---|---|
| SCP-01 | データスコープ(見える行)と項目スコープ(見える列)を、ともに lib/repo/_scope.ts に集約すること |
| SCP-02 | コンポーネント側で role === 'staff' のような分岐を書かないこと |
| SCP-03 | 原価・在庫金額・粗利は、権限がない場合そもそもデータに含めずに返すこと(CSSで隠さない) |
ロールを跨ぐ体験(必ず動くようにする)
- 現場担当で出庫を打つ → 在庫管理者に切替 → 在庫が減り、履歴にその取引が並んでいる
- 現場担当が棚卸で実数を入力 → 在庫管理者に切替 → 差異一覧に出る。承認するまで在庫は動かない → 承認 → 調整トランザクションが登録され、在庫が動く
- 在庫管理者が発注を登録 → 入荷予定が増え、推奨発注リストからその商品が消える
- 在庫管理者が引当を作る → 現場担当に切替 → 実在庫は変わらないが、有効在庫が減っている
- 在庫管理者が発注点を変更 → 推奨発注リストの件数が変わる
- 拠点間移動を起票 → 出荷元から減り「移動中」に計上され、入荷処理をするまで到着先に入らない
2番と4番が、このデモの核心。 1章の事実①②が体験として成立する瞬間。
デモ切替バー(F-C01)
画面上部に常時表示する固定バー。埋め込みモードでは非表示。
- 現在のロールと担当拠点の表示、3ロールの切替
- 現場担当ロール時:拠点の選択(東京/大阪の2拠点)
- 「デモをリセット」ボタン(確認ダイアログ付き)
- 元記事へ戻るリンク
- 「これはデモです。データは送信されません」の明示
デザイン上の扱い: プロダクト本体のUIとは意図的に別扱い(ダークな帯 + 小さめのタイポ)にし、「アプリの外側にある操作パネル」であることを視覚的に区別する。
5. 画面一覧とユーザーフロー
全32画面。画面IDはディレクトリ構成と 1:1 で対応させる。
在庫照会・マスタ
| 画面ID | 画面名 | パス | 主要要素 |
|---|---|---|---|
| SC-001 | ダッシュボード | / |
欠品リスク、期限アラート、要発注、未承認の棚卸差異、在庫金額 |
| SC-010 | 在庫一覧 | /stock |
4つの在庫数 + 行内水位バー、絞り込み、列設定、保存ビュー、一括操作 |
| SC-011 | 商品詳細 | /items/[sku] |
4数値ブロック、フル幅水位バー、拠点別・ロット別内訳、発注点、回転率 |
| SC-012 | 在庫履歴(元帳) | /items/[sku]/history |
全トランザクションの時系列。残高列で積み上げを可視化 |
| SC-013 | 商品の登録・編集 | /items/[sku]/edit |
SKU、JAN、単位換算、原価、発注点、保管条件 |
| SC-014 | CSVインポート | /import |
ファイル選択 → 列マッピング → プレビュー → 取込結果 |
| SC-020 | 拠点・ロケーション | /locations |
拠点一覧、棚番の階層、巡回順 |
| SC-021 | 取引先 | /partners |
仕入先・出荷先、リードタイム、最小発注金額 |
入出庫・移動
| 画面ID | 画面名 | パス | 主要要素 |
|---|---|---|---|
| SC-100 | 入庫(検品) | /receive |
発注選択、連続スキャン、数量差異、ロット・期限入力 |
| SC-101 | 入庫の棚入れ | /receive/[id]/putaway |
棚番の指定、既存棚の推奨 |
| SC-102 | 出庫(ピッキング) | /ship |
出荷指示選択、棚番順リスト、FEFO推奨、実績入力 |
| SC-103 | ピッキングリスト印刷 | /ship/[id]/print |
棚番順、バーコード付き、PDF出力 |
| SC-104 | 引当 | /allocations |
受注への引当、引当解除、引当状況一覧 |
| SC-110 | 在庫移動 | /transfer |
拠点間・棚間、移動中在庫の一覧 |
| SC-111 | 移動の入荷処理 | /transfer/[id]/receive |
到着確認、差異登録 |
| SC-120 | 在庫調整 | /adjust |
増減、理由コード必須、証跡写真 |
| SC-121 | 取消(逆仕訳) | モーダル | 対象取引の確認、逆仕訳の登録 |
| SC-130 | トランザクション一覧 | /transactions |
全取引、種別・拠点・期間・担当者で絞り込み、CSV出力 |
棚卸
| 画面ID | 画面名 | パス | 主要要素 |
|---|---|---|---|
| SC-200 | 棚卸一覧 | /stocktakes |
進行中・完了、対象範囲、進捗率、差異サマリ |
| SC-201 | 棚卸の開始 | /stocktakes/new |
対象範囲、担当者割当、理論在庫の凍結 |
| SC-202 | カウント入力 | /stocktakes/[id]/count |
棚番順リスト、バーコード、実数入力、未カウント件数 |
| SC-203 | 差異一覧 | /stocktakes/[id]/variance |
理論 vs 実数、差異金額、差異率ソート、原因分類 |
| SC-204 | 承認と確定 | /stocktakes/[id]/approve |
最終確認、承認、調整トランザクションの生成 |
| SC-205 | 棚卸履歴 | /stocktakes/history |
過去の棚卸、差異推移、精度の改善度 |
発注・期限
| 画面ID | 画面名 | パス | 主要要素 |
|---|---|---|---|
| SC-300 | 推奨発注リスト | /replenishment |
発注点を下回る商品、推奨数量、仕入先別グループ |
| SC-301 | 発注書の作成 | /purchase-orders/new |
仕入先別、数量調整、PDF出力 |
| SC-302 | 発注一覧・入荷予定 | /purchase-orders |
発注残、予定日、遅延アラート |
| SC-310 | 期限アラート | /expiry |
期限切れ間近・期限切れのロット、閾値設定 |
| SC-311 | ロット一覧 | /lots |
ロット別在庫、入荷日、期限、拠点 |
分析・設定
| 画面ID | 画面名 | パス | 主要要素 |
|---|---|---|---|
| SC-400 | 在庫回転率・滞留 | /reports/turnover |
回転率、最終出庫からの経過日数、滞留金額 |
| SC-401 | ABC分析 | /reports/abc |
累積構成比のパレート図、A/B/C区分 |
| SC-402 | 在庫金額推移 | /reports/valuation |
移動平均法での評価額、月次推移、拠点別 |
| SC-403 | 欠品分析 | /reports/stockout |
欠品回数、機会損失の推定 |
| SC-404 | 差異分析 | /reports/variance |
原因別・拠点別・担当者別の差異金額 |
| SC-500 | 設定 | /settings |
発注点の一括設定、期限閾値、理由コード、単位 |
| SC-501 | ユーザー・権限 | /settings/users |
メンバー、ロール、担当拠点 |
| SC-502 | 操作ログ | /settings/logs |
操作者・日時・対象・内容の検索 |
| SC-900 | 埋め込みモード | /?embed=1 |
1商品の水位バー + 入出庫パネル(2章) |
主要フロー
フローA:記事の読者が「4つの在庫数」を理解する(最重要)
SEO記事を読んでいる
↓
記事内の埋め込み(SC-900)
┌────────────────────────────────────────────────┐
│ SKU-1042 ステンレスボトル 500ml マット黒 │
│ │
│ 実在庫 120 − 引当済 30 = 有効在庫 90 │
│ 入荷予定 60 │
│ │
│ ├─欠品─┼─発注点─┼──────適正──────┼─過剰─┤ │
│ 0 40 60 200 300 │
│ ▓▓▓▓▓▓▓▓▓▓▓░░░ 120(うち斜線=引当30) │
│ ▒▒▒▒▒ +60(入荷予定) │
│ │
│ [ 入庫 +10 ] [ 出庫 -10 ] [ 引当 +10 ] │
└────────────────────────────────────────────────┘
↓
「引当 +10」を押す
↓ ★ 実在庫は 120 のまま、有効在庫だけが 90 → 80 に減る
★ 「棚にあるのに売れない在庫」が目で見える
↓
「出庫 -10」を押す
↓ ★ 実在庫が 110 に減り、水位バーの塗りが下がる
↓
「全画面で試す」→ 別タブ
↓
SC-010 在庫一覧 ── 620 SKU すべてに4つの在庫数と水位バーが並んでいる
↓
SC-012 在庫履歴 ── ★ 残高列があり、現在庫がどの取引の積み上げかが追える
↓
ロール切替「現場担当」→ ★ 原価と在庫金額の列が消え、拠点も絞られる
↓
資料ダウンロード or 元記事に戻る
フローB:棚卸で差異を見つけて直す(1章の事実①を体験に落とす)
【在庫管理者】SC-201 棚卸の開始
対象:東京倉庫 / エリアA / 担当:現場担当2名
↓ ★ 開始した瞬間、対象SKUの理論在庫が凍結される
【現場担当】SC-202 カウント入力
棚番順のリストが出る(A-01-1 → A-01-2 → …)
バーコードをスキャン → 実数を入力(1画面で連続入力)
↓ ★ 未カウント件数が減っていく
SKU-1042:理論 120 / 実数 116 と入力
↓
【在庫管理者】SC-203 差異一覧
差異率が大きい順に並ぶ
SKU-1042 理論120 → 実数116 差異 −4 差異金額 −¥5,200
↓
★ 原因分類が必須。未分類があると承認に進めない
「破損(廃棄済み・記録漏れ)」を選択
↓
SC-204 承認と確定
★ 承認するまで在庫は動かない
承認 → 調整トランザクション(−4 / 理由:棚卸差異・破損)が登録される
↓
SC-012 在庫履歴 ── ★ 履歴に「棚卸差異 −4」が並び、残高が 116 になっている
↓
SC-404 差異分析 ── ★ 原因別の差異金額に「破損」が積み上がる
↓
★ ここで伝わること:
「在庫は必ずズレる。だから見つけて、原因を分類して、履歴に残す」
フローC:欠品と過剰発注を同時に防ぐ
SC-001 ダッシュボード ──「要発注 18件」「欠品リスク 3件」「遅延発注 2件」
↓
SC-300 推奨発注リスト
判定式:有効在庫 + 入荷予定 < 発注点
┌──────────────────────────────────────────────────┐
│ SKU 商品名 有効 入荷 発注点 推奨 │
│ 1042 ボトル500ml 80 60 60 - │ ← 入荷予定込みで足りている
│ 2011 キャップ黒 12 0 50 88 │ ← 発注が必要
└──────────────────────────────────────────────────┘
↓ ★ 「入荷予定」を見ていないと、SKU-1042 に重複発注してしまう
SC-301 発注書の作成 ── 仕入先別にまとめて、最小発注金額を確認、PDF出力
↓
発注登録 → ★ 入荷予定が増え、推奨発注リストから消える
↓
入荷日に SC-100 入庫(検品)
バーコードを連続スキャン → 予定数との差異を記録
ロット番号・賞味期限を入力
↓
SC-101 棚入れ ── 棚番を指定(同一SKUの既存棚を推奨)
↓
★ 実在庫が増え、入荷予定が減る
↓
SC-310 期限アラート ── ★ 入力した期限が近い場合、ここに現れる
↓
次の出庫時、SC-102 で ★ FEFO(期限が近いロットから)が推奨される
6. 機能要件
各要件はテスト仕様書の項目と1:1で対応する。「〜できること」の粒度で、判定可能な形で記述している。
6.1 在庫算出(このデモの心臓部)
| ID | 要件 | 優先 |
|---|---|---|
| FR-101 | 在庫数量をストアに保存せず、トランザクションから算出すること(INV-01) | P1 |
| FR-102 | 4つの在庫数(実在庫・引当済・有効在庫・入荷予定)を同じ関数から返すこと(INV-06) | P1 |
| FR-103 | 在庫を「SKU × 拠点 × ロット」の粒度で算出でき、任意の軸で集約できること | P1 |
| FR-104 | 在庫を変更する唯一の方法をトランザクションの追加とすること(INV-02) | P1 |
| FR-105 | トランザクションを削除・編集しないこと。 取消は逆仕訳の追加で行うこと(INV-03) | P1 |
| FR-106 | 任意の時点の在庫(過去日の在庫)を算出できること | P2 |
| FR-107 | 算出をメモ化し、トランザクション追加時に必ず無効化すること(INV-04) | P1 |
| FR-108 | lib/inventory/ を純粋関数とし、現在時刻を引数で受け取ること(INV-05) |
P1 |
| FR-109 | 620 SKU × 2拠点 × 5,000トランザクションの規模で、在庫一覧の算出が 150ms以下であること | P1 |
| FR-110 | 単位換算(ケース/ボール/バラ)を持ち、入庫はケース、出庫はバラの運用に対応すること。内部は常に最小単位で保持すること | P2 |
| FR-111 | 移動平均法で原価を算出し、在庫金額を計算すること。入庫のたびに単価を更新すること | P2 |
6.2 一覧・検索・ビュー(業務システムの中核)
| ID | 要件 | 優先 |
|---|---|---|
| FR-201 | 在庫一覧に、4つの在庫数と行内水位バーを同時表示すること | P1 |
| FR-202 | 表示する列をユーザーが選択・並び替えできること。設定は保持されること | P1 |
| FR-203 | 絞り込みは項目ごとの条件指定(等しい/含む/以上/以下/期間/空である)に対応すること | P1 |
| FR-204 | 在庫状態での絞り込みを提供すること(欠品/発注点以下/適正/過剰/滞留/期限間近) | P1 |
| FR-205 | 絞り込み条件はURLクエリに反映し、リロード・URL共有で再現できること | P1 |
| FR-206 | 適用中の絞り込み条件をチップで表示し、個別に解除できること | P1 |
| FR-207 | 絞り込み条件 + 表示列 + 並び順を「ビュー」として保存できること。個人/共有(在庫管理者以上)を区別すること | P1 |
| FR-208 | 保存ビューをサイドバーに一覧表示し、件数バッジ付きでワンクリック切替できること | P1 |
| FR-209 | 行のチェックボックス選択で一括操作(カテゴリ変更/発注点変更/棚番変更)ができること。絞り込み結果全件選択に対応すること | P2 |
| FR-210 | 一括操作の実行前に、対象件数と操作内容の確認ダイアログを表示すること | P1 |
| FR-211 | ヘッダーのグローバル検索から、SKU・商品名・JAN・ロット番号を横断検索できること | P1 |
| FR-212 | ⌘K / Ctrl+K でコマンドパレットを開き、検索・遷移・入出庫の起動ができること |
P2 |
| FR-213 | 行数が多くても描画がもたつかないこと(仮想スクロールを実装すること) | P1 |
| FR-214 | 行の高さを「標準(38px)/コンパクト(30px)」で切り替えられること | P2 |
| FR-215 | 数量列を右寄せ・等幅数字で表示し、桁を揃えること | P1 |
6.3 入出庫・移動
| ID | 要件 | 優先 |
|---|---|---|
| FR-301 | 入庫を発注書から起票でき、予定数と実数の差異を記録できること。部分入荷に対応すること | P1 |
| FR-302 | バーコードの連続スキャンで、同一SKUの数量を積み上げられること。 スキャンごとに画面が遷移しないこと | P1 |
| FR-303 | バーコードはキー入力を既定とし、カメラは任意で有効化すること(ハンディ端末はキーボードとして振る舞うため、キー入力を主にしておけばハード追加時にソフト改修が不要になる) | P1 |
| FR-304 | 未登録のバーコードをスキャンした場合、その場で商品を登録するか、保留にするかを選べること | P2 |
| FR-305 | ロット管理対象の商品では、入庫時にロット番号と期限の入力を必須とすること | P1 |
| FR-306 | 棚入れで棚番を指定でき、同一SKUの既存棚を推奨表示すること | P2 |
| FR-307 | 出庫を出荷指示から起票でき、ピッキングリストを棚番の巡回順に並べること | P1 |
| FR-308 | ロット管理対象の出庫では、FEFO(期限が近い順)でロットを推奨し、逸脱時は理由の確認を求めること | P1 |
| FR-309 | 出庫数が有効在庫を超える場合、警告を出すこと。ただしブロックはしない(現場の実態を優先し、調整で辻褄を合わせられるようにする) | P1 |
| FR-310 | 引当を作成・解除でき、実在庫は変えずに有効在庫のみを減らすこと | P1 |
| FR-311 | 拠点間移動では、出荷時点で出荷元から減算し「移動中」に計上、到着時の入荷処理で到着先に加算すること | P1 |
| FR-312 | 移動中在庫の一覧と滞留日数を確認できること | P2 |
| FR-313 | 棚間移動(同一拠点内)では、移動中状態を持たず即座に反映すること | P2 |
| FR-314 | 在庫調整では理由コードを必須とすること(破損/紛失/期限切れ廃棄/誤記訂正/その他) | P1 |
| FR-315 | 取消は逆仕訳の追加として実装すること。元の取引に「取消済」の印を付け、履歴から消さないこと | P1 |
| FR-316 | オフラインでも入出庫を記録でき、復帰時に自動同期すること。未同期件数をバッジ表示すること | P2 |
| FR-317 | すべての取引に、担当者・拠点・日時・端末種別を記録すること | P1 |
6.4 棚卸
| ID | 要件 | 優先 |
|---|---|---|
| FR-401 | 棚卸を対象範囲(拠点/棚エリア/カテゴリ)を指定して開始でき、開始時点の理論在庫を凍結すること | P1 |
| FR-402 | 棚卸中も通常の入出庫を継続できること。 凍結後の取引は差異計算から除外し、別途表示すること | P1 |
| FR-403 | カウント入力を棚番順のリストで行えること。バーコードスキャンと手入力の両対応とすること | P1 |
| FR-404 | 未カウント件数・進捗率を常時表示すること | P1 |
| FR-405 | 差異一覧で、理論在庫・実数・差異数・差異金額・差異率を表示し、差異率でソートできること | P1 |
| FR-406 | 差異の原因分類を必須とすること(記録漏れ/破損/紛失/誤出荷/棚違い/不明)。未分類があると承認に進めないこと | P1 |
| FR-407 | 「棚違い」を選んだ場合、対になる差異(同数の過不足)を候補提示すること | P3 |
| FR-408 | 承認するまで在庫を動かさないこと。 承認時に調整トランザクションを生成すること | P1 |
| FR-409 | 承認は在庫管理者以上のみが行えること。カウントした本人が単独で承認できない設定を提供すること | P2 |
| FR-410 | 棚卸履歴で、過去の差異推移と精度の改善度を確認できること | P2 |
| FR-411 | 循環棚卸として、ABC区分に応じた棚卸頻度を設定し、対象期日が来た範囲を提示すること | P3 |
6.5 発注・ロット期限
| ID | 要件 | 優先 |
|---|---|---|
| FR-501 | 商品ごとに発注点・安全在庫・発注ロットを設定できること | P1 |
| FR-502 | 発注点の推奨値を算出して提示すること(リードタイム × 平均出庫数 + 安全在庫) | P2 |
| FR-503 | 推奨発注リストの判定式を「有効在庫 + 入荷予定 < 発注点」とすること。 実在庫だけで判定しないこと | P1 |
| FR-504 | 推奨数量を算出すること(発注点までの不足数を発注ロットで切り上げ) | P1 |
| FR-505 | 推奨発注リストを仕入先別にグループ化し、最小発注金額に達しているかを表示すること | P2 |
| FR-506 | 発注書を作成でき、PDF出力できること | P2 |
| FR-507 | 発注残・入荷予定日を管理し、予定日を過ぎた発注を遅延として表示すること | P1 |
| FR-508 | ロット別在庫を保持し、入荷日・期限を記録すること | P1 |
| FR-509 | 期限アラートの閾値(何日前から警告するか)をカテゴリ別に設定できること | P1 |
| FR-510 | 期限切れ間近・期限切れのロットを一覧表示し、期限切れを在庫から除外するか含めるかを選べること | P1 |
| FR-511 | 期限切れロットの廃棄を、在庫調整(理由:期限切れ廃棄)として登録できること | P1 |
6.6 分析・設定
| ID | 要件 | 優先 |
|---|---|---|
| FR-601 | ダッシュボードに、欠品リスク・期限アラート・要発注・未承認の棚卸差異・在庫金額を表示すること | P1 |
| FR-602 | ダッシュボードの表示内容がロールによって変わること(現場担当は自拠点、金額は非表示) | P1 |
| FR-603 | 在庫回転率(出庫金額 ÷ 平均在庫金額)を商品別・カテゴリ別に表示すること | P1 |
| FR-604 | 滞留在庫を、最終出庫からの経過日数と滞留金額で一覧化すること | P1 |
| FR-605 | ABC分析:出庫金額の累積構成比でA/B/C区分を算出し、パレート図で表示すること | P2 |
| FR-606 | ABC区分を、棚卸頻度の設定に反映できること | P3 |
| FR-607 | 在庫金額の月次推移を、移動平均法での評価額で表示すること | P2 |
| FR-608 | 欠品分析:欠品の発生回数と、機会損失の推定額を表示すること | P2 |
| FR-609 | 差異分析:原因別・拠点別・担当者別の差異金額を表示すること | P2 |
| FR-610 | 全ての分析画面で、期間(今月/先月/四半期/任意)を指定できること | P2 |
| FR-611 | 理由コード・単位・期限閾値・発注点を設定画面から管理できること | P1 |
| FR-612 | ユーザーのロールと担当拠点を管理者が変更できること | P2 |
| FR-613 | 在庫を動かす操作(調整・棚卸承認・取消・マスタ変更)を操作ログに記録し、検索できること | P2 |
6.7 CSV入出力・デモ基盤
| ID | 要件 | 優先 |
|---|---|---|
| FR-701 | CSVインポートで、列マッピング(自動推測 + 手動修正)、プレビュー、エラー行の理由表示、部分取込ができること | P1 |
| FR-702 | 商品マスタ・在庫初期値・取引先の3種のインポートに対応すること | P1 |
| FR-703 | 在庫初期値のインポートは「期首棚卸」トランザクションとして登録すること。 在庫数を直接書き込まないこと | P1 |
| FR-704 | インポート時、数式インジェクション(=, +, -, @ 始まり)を無害化すること |
P1 |
| FR-705 | 現在の絞り込み条件・表示列のままCSVエクスポートでき、実際にファイルがダウンロードされること | P1 |
| FR-706 | 3ロールをワンクリックで切り替えられること | P1 |
| FR-707 | データスコープと項目スコープを lib/repo/_scope.ts に集約すること(SCP-01, SCP-02) |
P1 |
| FR-708 | 原価・在庫金額は、権限がない場合データに含めずに返すこと(SCP-03) | P1 |
| FR-709 | 「デモをリセット」で localStorage をクリアすること | P1 |
| FR-710 | 操作結果がリロード後も保持されること | P1 |
| FR-711 | ロール権限外の画面では案内画面から切り替えられること。素の404を出さないこと | P1 |
| FR-712 | 権限で不可の操作はボタンを非活性にし、理由をツールチップで示すこと。実行してからエラーを出さないこと | P1 |
| FR-713 | 元記事へ戻るリンクと資料ダウンロードを常設すること | P1 |
| FR-714 | 初回訪問時に3ステップのガイドツアーを表示し、「今後表示しない」を選べること | P2 |
| FR-715 | 埋め込みモード:1商品の在庫水位パネルと入出庫ボタンのみを表示し、カメラを呼ばず、アプリ内遷移をしないこと(EMB-04, EMB-05) | P1 |
7. データ設計
型定義(lib/types/)
// ---- 組織・ユーザー ----
type Warehouse = { id: string; code: string; name: string; address: string; isActive: boolean }
type Location = { // 棚番
id: string
warehouseId: string
code: string // 'A-01-1'(エリア-列-段)
area: string
storageCondition: 'normal' | 'chilled' | 'frozen'
sortOrder: number // ★ ピッキングの巡回順
}
type User = {
id: string
name: string
role: 'staff' | 'keeper' | 'admin'
warehouseIds: string[] // ★ 担当拠点。スコープ判定の起点
isActive: boolean
}
// ---- マスタ ----
type Item = {
id: string
sku: string
name: string
jan?: string
categoryId: string
// --- 単位換算(FR-110) ---
baseUnit: string // 最小単位(「本」)
packUnits: { name: string; qtyInBase: number }[] // [{ケース,24},{ボール,6}]
// --- 原価・売価(★ 権限で出し入れする項目)---
cost: number // 移動平均原価
price: number
// --- 補充 ---
reorderPoint: number
safetyStock: number
orderLot: number
defaultSupplierId?: string
leadTimeDays: number
// --- 管理方式 ---
isLotManaged: boolean // ★ ロット・期限管理の対象か
isSerialManaged: boolean
shelfLifeDays?: number
storageCondition: 'normal' | 'chilled' | 'frozen'
abcClass?: 'A' | 'B' | 'C' // 算出値だが検索性のため保持
isActive: boolean
}
type Category = { id: string; name: string; expiryAlertDays: number; sortOrder: number }
type Partner = { id: string; kind: 'supplier' | 'customer'; code: string; name: string; leadTimeDays: number; minOrderAmount: number }
type ReasonCode = { id: string; kind: 'adjust' | 'variance'; name: string; sortOrder: number }
type Lot = { id: string; itemId: string; lotNo: string; receivedAt: string; expiryDate?: string; supplierId?: string }
// ---- ★★★ トランザクション(このプロジェクトの中核)★★★ ----
type TxnType =
| 'receive' // 入庫
| 'ship' // 出庫
| 'transfer_out' // 拠点間移動:出荷
| 'transfer_in' // 拠点間移動:入荷
| 'move' // 棚間移動
| 'adjust' // 在庫調整
| 'stocktake' // 棚卸差異(承認済みのみ生成)
| 'opening' // 期首棚卸(初期在庫の投入)(FR-703)
type Transaction = {
id: string
seq: number // ★ 連番。メモ化の世代管理に使う(INV-04)
type: TxnType
itemId: string
warehouseId: string
locationId?: string
lotId?: string
qtyBase: number // ★ 常に最小単位。符号付き(入庫 +、出庫 −)
inputQty: number // 入力された数量
inputUnit: string // 入力された単位(ケース等)
unitCost?: number // 入庫時の単価(移動平均の計算に使う)
reasonCodeId?: string // ★ adjust / stocktake では必須
refType?: 'purchase_order' | 'shipping_order' | 'transfer' | 'stocktake'
refId?: string
// --- 取消(FR-315)---
reversedByTxnId?: string // この取引を打ち消した逆仕訳
reversesTxnId?: string // この取引が打ち消している元取引
// --- 監査 ---
userId: string
device: 'pc' | 'mobile' | 'scanner'
occurredAt: string
createdAt: string
note: string
syncState: 'synced' | 'pending'
}
// ---- 引当(★ 実在庫を動かさない)----
type Allocation = {
id: string
itemId: string
warehouseId: string
lotId?: string
qtyBase: number // 正の数
refType: 'shipping_order'
refId: string
status: 'active' | 'released' | 'shipped'
createdAt: string
releasedAt?: string
}
// ---- 発注 / 入荷予定 ----
type PurchaseOrder = {
id: string
code: string
supplierId: string
warehouseId: string
status: 'draft' | 'ordered' | 'partial' | 'received' | 'cancelled'
orderedAt?: string
expectedAt?: string // ★ 遅延判定の基準(FR-507)
lines: PurchaseOrderLine[]
createdAt: string
}
type PurchaseOrderLine = {
id: string
itemId: string
qtyBase: number
receivedQtyBase: number // ★ 入荷予定 = qtyBase − receivedQtyBase
unitCost: number
}
type ShippingOrder = {
id: string
code: string
customerId: string
warehouseId: string
status: 'draft' | 'allocated' | 'picking' | 'shipped' | 'cancelled'
shipBy?: string
lines: { id: string; itemId: string; qtyBase: number; shippedQtyBase: number }[]
createdAt: string
}
type Transfer = {
id: string
code: string
fromWarehouseId: string
toWarehouseId: string
status: 'in_transit' | 'received' | 'cancelled'
shippedAt: string
receivedAt?: string
lines: { itemId: string; lotId?: string; qtyBase: number; receivedQtyBase: number }[]
}
// ---- 棚卸 ----
type Stocktake = {
id: string
code: string
warehouseId: string
scope: { areas?: string[]; categoryIds?: string[] }
status: 'counting' | 'reviewing' | 'approved' | 'cancelled'
frozenAt: string // ★ 理論在庫を凍結した時刻(FR-401)
assigneeIds: string[]
lines: StocktakeLine[]
approvedBy?: string
approvedAt?: string
createdAt: string
}
type StocktakeLine = {
id: string
itemId: string
locationId?: string
lotId?: string
theoreticalQty: number // ★ 凍結時点の理論在庫
countedQty?: number // 未入力なら undefined
countedBy?: string
countedAt?: string
varianceQty?: number // countedQty − theoreticalQty
varianceAmount?: number
reasonCodeId?: string // ★ 必須(FR-406)
note: string
}
// ---- 在庫スナップショット(★ 保存しない。常に算出)----
type StockSnapshot = {
itemId: string
warehouseId?: string // 未指定なら全拠点合計
lotId?: string
// ★ 4つの在庫数(FR-102, INV-06)
onHand: number // 実在庫
allocated: number // 引当済
available: number // 有効在庫 = onHand − allocated
incoming: number // 入荷予定(発注残 + 移動中)
inTransit: number // うち移動中
reorderPoint: number
safetyStock: number
status: 'stockout' | 'below_reorder' | 'normal' | 'excess'
// --- 金額(★ 権限がない場合は含めない。SCP-03)---
unitCost?: number
stockValue?: number
// --- 滞留・期限 ---
lastShippedAt?: string
idleDays?: number
nearestExpiryDate?: string
expiringQty?: number
}
// ---- 分析(算出値。ストアに保存しない)----
type TurnoverMetrics = { itemId: string; shippedValue: number; avgStockValue: number; turnover: number; idleDays: number }
type AbcResult = { itemId: string; shippedValue: number; cumulativeRatio: number; abcClass: 'A'|'B'|'C' }
type VarianceSummary = { reasonCodeId: string; count: number; varianceQty: number; varianceAmount: number }
// ---- ビュー・監査 ----
type SavedView = {
id: string
name: string
target: 'stock' | 'transactions' | 'lots' | 'replenishment'
ownerId: string
isShared: boolean // keeper 以上のみ true にできる
filters: Filter[]
visibleColumns: string[]
sort: { field: string; dir: 'asc' | 'desc' }[]
}
type Filter = { field: string; operator: 'eq'|'contains'|'gte'|'lte'|'between'|'in'|'isEmpty'; value: unknown }
type AuditLog = {
id: string
actorId: string
action: 'adjust' | 'approve_stocktake' | 'reverse_txn' | 'update_master' | 'import'
targetType: string
targetId: string
changes: { field: string; before: unknown; after: unknown }[]
createdAt: string
}
在庫算出の実装(7章の中核)
// lib/inventory/snapshot.ts
export function getSnapshot(input: SnapshotInput): StockSnapshot {
const { itemId, warehouseId, txns, allocations, purchaseOrders, transfers, item } = input
// ★ 在庫は必ずトランザクションの合計として算出する(INV-01)
const onHand = txns
.filter(t => t.itemId === itemId && matchWarehouse(t, warehouseId) && !t.reversedByTxnId)
.reduce((sum, t) => sum + t.qtyBase, 0)
// ★ 引当は実在庫を動かさない。有効在庫だけを減らす(FR-310)
const allocated = allocations
.filter(a => a.itemId === itemId && a.status === 'active' && matchWh(a, warehouseId))
.reduce((sum, a) => sum + a.qtyBase, 0)
// ★ 入荷予定 = 発注残 + 移動中(FR-503 の判定に使う)
const onOrder = purchaseOrders
.filter(po => po.status === 'ordered' || po.status === 'partial')
.flatMap(po => po.lines.filter(l => l.itemId === itemId)
.map(l => l.qtyBase - l.receivedQtyBase))
.reduce((a, b) => a + b, 0)
const inTransit = transfers
.filter(t => t.status === 'in_transit' && t.toWarehouseId === warehouseId)
.flatMap(t => t.lines.filter(l => l.itemId === itemId)
.map(l => l.qtyBase - l.receivedQtyBase))
.reduce((a, b) => a + b, 0)
return {
itemId, warehouseId,
onHand,
allocated,
available: onHand - allocated, // ★ 有効在庫
incoming: onOrder + inTransit, // ★ 入荷予定
inTransit,
reorderPoint: item.reorderPoint,
safetyStock: item.safetyStock,
status: judgeStatus(onHand - allocated, item),
// 金額は _scope.ts で権限に応じて付与/除去する(SCP-03)
}
}
シードデータ(lib/seed/)
| ファイル | 内容 | 件数 |
|---|---|---|
warehouses.ts locations.ts |
拠点2(東京・大阪)、棚番(エリアA〜C × 列 × 段、巡回順付き) | 拠点2 / 棚120 |
users.ts |
現場担当3・在庫管理者2・管理者1 | 6名 |
categories.ts reasonCodes.ts partners.ts |
カテゴリ8、理由コード8、仕入先12・出荷先20 | — |
items.ts |
商品(うち3割をロット・期限管理対象。単位換算あり/なしを混在) | 620 SKU |
lots.ts |
ロット(期限切れ・期限間近・余裕あり を分散) | 約400件 |
transactions.ts |
期首棚卸 + 過去180日の入出庫・移動・調整 | 約5,000件 |
allocations.ts |
引当(有効在庫が実在庫より少ない商品を作る) | 約90件 |
purchaseOrders.ts |
発注(発注済・部分入荷・遅延あり を分散) | 45件 |
shippingOrders.ts |
出荷指示(引当済・ピッキング中・出荷済) | 60件 |
transfers.ts |
拠点間移動(移動中のものを数件残す) | 12件 |
stocktakes.ts |
棚卸(完了2回 + カウント中1件 + 未承認の差異あり1件) | 4件 |
シード作成のルール(品質を左右する)
- 実在企業名・実在商品名を使わない。 架空の商品・取引先で構成する
- 商品名を「商品A」のようなダミーにしない。 業種の世界観を持たせる(「ステンレスボトル 500ml マット黒」等)。ここが手抜きだとデモ全体が安っぽくなる
- 在庫状態を全帯域に分散させる。 欠品/発注点以下/適正/過剰/滞留が必ず存在すること
- 引当済がある商品を必ず作る。 「実在庫はあるのに有効在庫が足りない」商品がないと、1章の事実②が伝わらない
- 入荷予定がある商品を作る。 「発注済みなので発注不要」が推奨発注リストで表現されること
- 期限切れ・期限間近のロットを作る。 期限アラートが空だと機能が死ぬ
- 移動中の在庫を残す。 「どこにもない在庫」の存在を見せる
- 未承認の棚卸差異を1件残す。 ダッシュボードのアラートが動いて見える
- 差異の原因を分散させる。 差異分析のグラフに意味のあるパターンが出ること
- 遅延している発注を作る。 全部順調だとアラート機能が動いて見えない
- 日付は現在日時からの相対で生成する。固定日付を埋め込まない
- トランザクションは時系列に矛盾がないこと(在庫がマイナスになる履歴を作らない)
ストアとリポジトリ
lib/
├── types/ seed/
├── inventory/ # ★ 在庫算出の唯一の場所。純粋関数のみ
│ ├── snapshot.ts # ★ 4つの在庫数(FR-102)
│ ├── ledger.ts # 履歴と残高推移(FR-106)
│ ├── valuation.ts # 移動平均原価・在庫金額(FR-111)
│ ├── unit.ts # 単位換算(FR-110)
│ ├── fefo.ts # 期限順のロット選択(FR-308)
│ ├── replenish.ts # 発注点判定・推奨数量(FR-503, FR-504)
│ └── status.ts # 在庫状態の判定
├── analytics/ # 回転率・滞留・ABC・差異・欠品(純粋関数)
├── csv/ # インポート(列マッピング・検証・無害化)/エクスポート
├── scan/ # バーコード入力(キー入力・カメラ)
├── query/ # 絞り込み・ソート・ビューの適用
├── store/ { session, data, settings, sync, ui }
└── repo/ # ★ コンポーネントが触るのはここだけ
├── _delay.ts
├── _scope.ts # ★ データスコープ + 項目スコープ(SCP-01)
├── stock.ts items.ts lots.ts transactions.ts
├── receive.ts ship.ts allocations.ts transfers.ts adjust.ts
├── stocktakes.ts purchaseOrders.ts views.ts analytics.ts settings.ts
リポジトリ層の規約(厳守)
- 全メソッドを
asyncで定義する。中身が同期でも例外なく - 戻り値は
{ ok: true; data: T } | { ok: false; error: string }に統一する - コンポーネントから Zustand ストアを直接参照しない。 読み取りも書き込みもリポジトリ経由
- 在庫算出を
lib/inventory/の外に書かない(INV-01) - 在庫を変えるのはトランザクションの追加だけ(INV-02)
- スコープ適用(
_scope.ts)を全メソッドが必ず通す。 ここが実案件で権限制御・RLS に置き換わる箇所 - 原価・在庫金額は、権限がない場合オブジェクトから除外して返す(SCP-03)
- 派生値(在庫数・回転率・ABC区分・差異金額)はストアに保存せず算出する
- 在庫の再算出と一覧の絞り込みに擬似ディレイを入れない
8. デザイン要件
アートディレクション
「表の中に、在庫の水位を描く」
在庫管理システムの画面には、常に数字が並ぶ。しかし人が知りたいのは数字そのものではなく、「多いのか、少ないのか、あとどれくらいか」である。KURA は一覧のすべての行に在庫の水位バーを埋め込み、欠品線・発注点・現在庫・入荷予定を1本の帯で読ませる。
- 数字と図を同じ行に置く。 表とグラフを別画面に分けない
- 色は在庫状態のためだけにある。 通常状態はほぼ無彩色
- 産業的なトーン。 倉庫・工場の色(セメントのグレー、ティール、注意喚起のアンバー)
- 数値は縦長書体で。 桁数の多い数量が並んでも列幅を食わない
RELATE との差別化(最重要) どちらも「白背景・高密度・業務システム」だが、方向を分ける。 RELATE:罫線と等幅数字(Roboto Mono)で表を読ませる。藍。色は警告のためだけ。 KURA:表の各行に水位バーを描く。ティール+アンバー。数値は縦長(Roboto Condensed)。 並べたときに「同じ管理画面」と見えたら失敗。
カラートークン
:root {
/* 産業的なクールグレー */
--bg: #EEF0F1; /* ページ背景(セメント) */
--panel: #FFFFFF; /* テーブル・カード */
--panel-alt: #F5F7F8; /* 縞・ヘッダー */
--line: #DBDFE2; /* 罫線 */
--line-hi: #B9C0C5; /* テーブルヘッダー下 */
--ink-900: #10161C; /* 見出し・本文・数値 */
--ink-600: #4E585F; /* 補助テキスト */
--ink-400: #8B959C; /* ラベル・非活性 */
--primary: #0E7C86; /* 主要CTA・選択状態(ティール) */
--primary-d: #0A5C64;
--primary-50:#E2F0F1;
/* ★ 在庫状態。これ以外の色を意味づけに使わない */
--st-stockout: #B3352B; /* 欠品 */
--st-low: #C9821B; /* 発注点以下(産業安全色のアンバー) */
--st-normal: #3E7A5E; /* 適正 */
--st-excess: #6B6FA8; /* 過剰 */
--st-idle: #8B959C; /* 滞留(無彩色) */
/* ★ 水位バーの構成色 */
--gauge-track: #E4E8EA; /* 目盛の地 */
--gauge-onhand: #0E7C86; /* 実在庫(塗り) */
--gauge-allocated:repeating-linear-gradient( /* ★ 引当済(斜線) */
45deg, transparent 0 4px, rgba(14,124,134,.30) 4px 8px);
--gauge-incoming: #A9C6C9; /* 入荷予定(淡色) */
--gauge-reorder: #C9821B; /* 発注点ライン */
--gauge-safety: #B3352B; /* 安全在庫ライン */
/* 期限 */
--exp-expired: #B3352B;
--exp-near: #C9821B;
}
配色ルール
- 通常状態の行に色を持たせない。 色がついている行は「対応が必要な行」だけ
- 引当済は色ではなく斜線で表す。 「実在庫の中に含まれるが、使えない部分」であることを、塗りとテクスチャの違いで示す
- 過剰在庫を赤にしない。 赤は欠品と期限切れにだけ使う
- カテゴリに固有色を割り当てない。8色のバッジが並ぶ画面にしない
タイポグラフィ
| 役割 | 書体 | 用途 |
|---|---|---|
| UI全般 | Noto Sans JP 400 / 700 | 見出しも本文もこれ1つ。ウェイトと文字サイズで階層を作る |
| 数値 | Roboto Condensed 600(font-variant-numeric: tabular-nums) |
数量・金額・差異・日数。縦長で、桁数が多くても列幅を食わない |
| トークン | サイズ | 用途 |
|---|---|---|
| page-title | 22px / 700 | 画面タイトル |
| section | 15px / 700 | セクション見出し |
| body | 13px / 400 | テーブルセル、本文 |
| label | 11px / 700(letter-spacing:.05em) |
項目ラベル |
| qty-lg | 30px / 600 Condensed | 商品詳細の主要在庫数、ダッシュボードの指標 |
| qty | 14px / 600 Condensed | テーブル内の数量・金額 |
| qty-sm | 12px / 600 Condensed | 補助的な数量(入荷予定など) |
Roboto Condensed が KURA の視覚的な指紋。 RELATE の Roboto Mono(等幅)とは字幅が明確に違い、並べたときに一目で区別できる。
レイアウトとスペーシング
| 項目 | 定義 |
|---|---|
| スペーシング | 4pxベース:4 / 8 / 12 / 16 / 24 / 32 / 48 |
| アプリシェル | 左サイドバー(232px、折りたたみ56px)+ メインエリア。ヘッダー52px固定 |
| コンテンツ幅 | 最大幅を設けない。 画面幅いっぱいに使う |
| テーブル行高 | 標準38px / コンパクト30px(切替可能) |
| 水位バー | 行内では 高さ6px × 幅120px。商品詳細では 高さ14px × 幅いっぱい |
| 数量列 | 右寄せ。 単位は数値の後ろに --ink-400 で小さく |
| 角丸 | 3px(カード・入力・ボタン)/2px(バッジ)/0(水位バー) |
| 影 | ポップオーバー・モーダルのみ(0 4px 12px rgba(16,22,28,.12))。テーブルやカードに影を使わない |
主要コンポーネント仕様
| コンポーネント | 仕様 |
|---|---|
| 在庫水位バー(最重要) | 目盛の地の上に、実在庫の塗り(--gauge-onhand)/引当済の斜線/入荷予定の淡色を積む。発注点に縦線(アンバー)、安全在庫に縦線(赤)。ホバーで4つの数値をツールチップ表示 |
| 4数値ブロック | 実在庫/引当済/有効在庫/入荷予定 を横並び。有効在庫のみ大きく(qty-lg)。実在庫と引当済の間に「−」、有効在庫の前に「=」を置いて関係を示す |
| データテーブル | 固定ヘッダー、行ホバーで --primary-50 の50%、選択行は --primary-50。数量列は右寄せ + Condensed。列幅リサイズ可 |
| ツールバー | テーブル上部に固定。左=ビュー切替・絞り込み・列設定、右=密度切替・エクスポート・新規起票 |
| 在庫状態バッジ | 5状態(欠品/発注点以下/適正/過剰/滞留)。色 + テキストを併記 |
| スキャン入力欄 | 常時フォーカス。スキャンごとに下に行が積み上がる(画面は遷移しない)。直前の行をハイライトし数量を +1。取り消しボタンを各行に置く |
| ピッキングリスト行 | 棚番(大きく)/SKU/商品名/指示数(qty)/実数入力/ロット。巡回順に並び、次に行くべき棚を強調 |
| 差異行 | 理論/実数/差異(符号付き・色付き)/差異金額/原因分類のセレクト。原因未選択の行は左端にアンバーの帯 |
| 元帳行 | 日時/種別/数量(符号付き)/残高/担当/参照。残高列があることが「積み上げ」の可視化 |
| 一括操作バー | 行を選択するとテーブル下部からせり上がる。「N件を選択中」+ 操作ボタン + 解除 |
| 期限バッジ | 「あと3日」「期限切れ」。--exp-near / --exp-expired |
| メトリクスカード | ラベル(label)/主数値(qty-lg)/前期比。装飾グラフを載せない |
| 空状態 | 全一覧に専用の空状態。「データ0件」と「絞り込み0件」を別の文言にする |
| スケルトン | テーブルは行形状。水位バーの位置も確保してレイアウトシフトを防ぐ |
| デモ切替バー | プロダクトのトークンとは別扱い(ダークな帯 + 小さめタイポ) |
インタラクション要件(業務システムの肝)
| ID | 要件 |
|---|---|
| IX-01 | 一覧の絞り込み・並び替えは体感で即座に反映されること(100ms以下) |
| IX-02 | テーブル上で ↑↓ 行移動、Enter 詳細を開く、Space 行選択、⌘A 全選択 |
| IX-03 | スキャン入力欄は常時フォーカスを保ち、スキャン後もフォーカスを失わないこと。 連続スキャンが止まらないこと |
| IX-04 | 数量入力はテンキーで完結すること(Enter で確定して次の行へ、Tab で次のセル) |
| IX-05 | ⌘K コマンドパレット、/ グローバル検索フォーカス、? ショートカット一覧 |
| IX-06 | 破壊的操作(調整・取消・一括更新)は、実行後に5秒間「元に戻す」を表示すること |
| IX-07 | 保存は楽観的更新とし、失敗時のみロールバックしてエラーを表示すること |
| IX-08 | モーダルを開いても背後の一覧の状態(スクロール位置・選択)が保持されること |
| IX-09 | 棚卸のカウント入力は、1画面で連続して打ち続けられること。 1件ごとに画面遷移させないこと |
モーション
| 対象 | duration | 内容 |
|---|---|---|
| ホバー・フォーカス | 80ms | 色のみ。業務システムでは反応が速いことが価値 |
| 水位バーの変化 | 400ms | 在庫が動いたときのみ。塗りの伸縮で見せる |
| モーダル・ポップオーバー | 160ms | フェード + 4px |
| 一括操作バーのせり上がり | 200ms | 下から |
| スキャン行の追加 | 120ms | 上から差し込む |
| 数値の変化 | 400ms | ダッシュボードの指標のみ。テーブル内では行わない |
| トースト | 160ms | 右下から |
prefers-reduced-motion: reduce 時は全アニメーションを無効化する。
アクセシビリティ(WCAG 2.1 AA)
| ID | 要件 |
|---|---|
| A11Y-01 | コントラスト比は通常4.5:1以上。13px本文でも必ず満たすこと |
| A11Y-02 | 全ての操作をキーボードのみで完遂できること |
| A11Y-03 | フォーカスリングは2px・オフセット2pxで常時可視。outline:none の単独使用禁止 |
| A11Y-04 | 在庫状態を色のみで伝えないこと。 バッジにテキストを併記すること |
| A11Y-05 | 水位バーは装飾ではなく情報である。 4つの在庫数を必ず数値としても表示し、バーに aria-label(「実在庫120、引当30、有効90、入荷予定60」)を持たせること |
| A11Y-06 | 引当済を色の違いだけで示さないこと(斜線テクスチャ + 数値表示) |
| A11Y-07 | テーブルは <table> を用い、scope と caption を適切に設定すること |
| A11Y-08 | フォームの各入力に label を関連付け、エラーは aria-describedby と role="alert" で読み上げること |
| A11Y-09 | モーダルはフォーカストラップ + Esc で閉じる + 起動元へフォーカス復帰 |
| A11Y-10 | スキャンの成功・失敗を aria-live="polite" で通知すること(画面を見ずに打てるように) |
| A11Y-11 | 一括操作の選択件数、絞り込み結果の件数を aria-live で通知すること |
| A11Y-12 | フォントサイズ200%指定でも内容が読め、テーブルは横スクロールで対応すること |
ライティング規約
| 原則 | 例 |
|---|---|
| 4つの在庫数を混同させない | ○「有効在庫 90(実在庫 120 − 引当 30)」/×「在庫 120」 |
| 件数と金額を明示する | ○「12件(差異金額 −¥48,200)を承認しますか?」/×「承認しますか?」 |
| エラーは原因と対処 | ○「3行目:数量が数値ではありません。半角数字で入力してください」/×「取込に失敗しました」 |
| 理由を求める理由を書く | ○「差異の原因を選んでください。原因別に集計して、再発を防ぐために使います」 |
| 警告はブロックしない | ○「有効在庫(90)を超える出庫です。続行すると在庫がマイナスになります」+ 続行ボタン |
| 空状態を区別する | データ0件:「まだ在庫がありません。CSVで取り込むか、入庫から始めましょう」/絞り込み0件:「条件に一致する商品はありません」+ 条件解除 |
| システム語を使わない | ○「この入庫を取り消す」/×「statusをreversedに更新」 |
9. 技術要件・ディレクトリ構成
技術スタック(固定・勝手に変更しない)
| レイヤ | 技術 | 備考 |
|---|---|---|
| フレームワーク | Next.js 15(App Router)/ TypeScript strict | |
| スタイリング | Tailwind CSS + CSS Variables | トークンは CSS 変数で定義 |
| UIコンポーネント | shadcn/ui(Radix UI基盤) | a11y要件を自前実装せずに満たす |
| 状態管理 | Zustand + persist | localStorage に永続化 |
| テーブル | TanStack Table v8 | 列制御・ソート・選択。UIは自前。ヘッドレスのまま使う |
| 仮想スクロール | TanStack Virtual | FR-213 |
| 在庫算出 | 自前実装(lib/inventory/) |
これが本プロジェクトの価値。ライブラリに任せない |
| 水位バー | 自前実装(div または SVG) | チャートライブラリを使わない。行内120pxの用途に重すぎる |
| バーコード | キー入力(既定)+ @zxing/browser(カメラ・動的import) |
FR-303 |
| フォーム | React Hook Form + Zod | |
| CSV | Papa Parse | インポート/エクスポート |
| @react-pdf/renderer(動的import) | 発注書・ピッキングリスト | |
| グラフ | Recharts(動的import) | ABC分析のパレート図、金額推移 |
| 日付 | date-fns(ja locale) | |
| アイコン | lucide-react | |
| コマンドパレット | cmdk | FR-212 |
| PWA | next-pwa | オフライン入出庫(FR-316) |
| ホスティング | Vercel | |
| テスト | Vitest / Playwright / axe-core |
上記以外のライブラリを入れる前に必ず提案し、承認を得ること。特に会計・在庫計算系のライブラリと、重量級チャートライブラリの導入は禁止。
ディレクトリ構成
/
├── CLAUDE.md
├── app/
│ ├── layout.tsx # アプリシェル(サイドバー・ヘッダー・デモ切替バー)
│ ├── page.tsx # SC-001 / SC-900(?embed=1)
│ ├── stock/ items/[sku]/ # SC-010〜013
│ ├── import/ # SC-014
│ ├── locations/ partners/ # SC-020, 021
│ ├── receive/ ship/ allocations/ transfer/ adjust/ # SC-100〜121
│ ├── transactions/ # SC-130
│ ├── stocktakes/ # SC-200〜205
│ ├── replenishment/ purchase-orders/ expiry/ lots/ # SC-300〜311
│ ├── reports/ # SC-400〜404
│ ├── settings/ # SC-500〜502
│ └── dev/components/
├── components/
│ ├── gauge/ # ★ 最重要
│ │ ├── StockGauge.tsx # 在庫水位バー(行内・詳細の2サイズ)
│ │ ├── FourNumbers.tsx # 4つの在庫数ブロック
│ │ └── GaugeTooltip.tsx
│ ├── table/ # DataTable, ColumnPicker, FilterBar, BulkActionBar, ViewSidebar
│ ├── scan/ # ScanInput(常時フォーカス), ScanLineList, CameraScanner
│ ├── domain/ # StockStatusBadge, ExpiryBadge, LedgerRow, VarianceRow, PickingRow, MetricCard
│ ├── demo/ # RoleSwitcher, ResetButton, GuideTour, BackToArticle
│ └── layout/
├── lib/
│ ├── types/ seed/ store/ repo/
│ ├── inventory/ # ★ 在庫算出。純粋関数のみ
│ ├── analytics/ # ★ 分析。純粋関数のみ
│ ├── csv/ scan/ query/ validation/ utils/
├── docs/
│ └── inventory-checklist.md # 資料PDFの元原稿(在庫要件チェックシート)
├── e2e/
└── public/
コーディング規約
- TypeScript strict。
any禁止。やむを得ない場合はunknown+ 型ガード - ファイル名は kebab-case、コンポーネントは PascalCase
- 1ファイル300行を超えたら分割を検討する
- コンポーネントから Zustand ストアを直接参照しない。必ず
lib/repo/経由 - 在庫算出を
lib/inventory/の外に書かない(INV-01) - 在庫を変えるのはトランザクションの追加だけ。数量を直接更新するコードを書かない(INV-02)
lib/inventory/とlib/analytics/は純粋関数のみ。 ストア・DOM に依存させず、new Date()を内部で呼ばない(INV-05)- 派生値をストアに保存しない。常に算出する
- 数量は常に最小単位(
qtyBase)・符号付きで保持し、表示時に単位換算する - 金額計算に浮動小数点の累積誤差を持ち込まない(原価は小数第2位で丸めた整数演算で扱う)
- コミットは1タスクごと。メッセージに要件IDを含める
例:
feat(inventory): 有効在庫の算出に引当を反映 (FR-102)
10. 非機能要件
| ID | 要件 | 目標値 |
|---|---|---|
| NFR-01 | LCP(デスクトップ) | 1.8秒以下 |
| NFR-02 | INP | 200ms以下 |
| NFR-03 | CLS | 0.1以下。水位バーの領域を最初から確保すること |
| NFR-04 | 在庫一覧の算出(620 SKU × 2拠点 × 5,000トランザクション) | 150ms以下 |
| NFR-05 | 一覧の絞り込み・並び替えの反映 | 100ms以下 |
| NFR-06 | 620行のスクロールが60fpsを維持すること(仮想スクロール) | — |
| NFR-07 | 連続スキャンの入力遅延 | 1件あたり50ms以下(ハンディ端末の連射に追従すること) |
| NFR-08 | 埋め込みモードの初期JSバンドル(gzip後) | 60KB以下 |
| NFR-09 | アプリ本体の初期JSバンドル(gzip後) | 250KB以下(PDF・グラフ・カメラは動的import) |
| NFR-10 | Lighthouse | Performance 90 / Accessibility 95 以上 |
| NFR-11 | 対応環境 | Chrome / Safari / Edge 最新2バージョン、iOS Safari 16以降、Android Chrome 最新 |
| NFR-12 | ユーザー入力をサニタイズし、CSVインポート時の数式インジェクションを無害化すること | — |
| NFR-13 | localStorage の容量上限に達した場合、警告を表示し、古いトランザクションを期首棚卸に要約して圧縮すること(データを破損させない) | — |
| NFR-14 | 入力された商品名・取引先名を外部に送信しないこと。 GA4 にも送らないこと(EMB-07) | — |
実案件への転用時の注意(デモ本体の要件ではない)
商談で聞かれた際に説明できるよう記録として残す。
- 在庫の同時更新はサーバー側で守る必要がある。 トランザクションを追加するだけの設計は競合しにくいが、「有効在庫を超えないことを保証する」なら排他制御が必要。本デモの
lib/inventory/がその判定ロジックの置き換え地点になる - 在庫は会計と繋がる。 在庫金額は決算に影響するため、原価評価の方法(移動平均/総平均/先入先出)は経理と合意してから実装する。後から変えると過去の評価額が全部変わる
- トランザクションは消さない。 監査対応上も、原因追跡上も、削除ではなく逆仕訳が原則
- 食品を扱う場合、ロットトレーサビリティが法令・取引先要求の対象になりうる。 回収時に「どのロットがどこに出たか」を遡れる設計が必要
- 本デモはデータをサーバーに送信しないため、これらの義務は発生しない。「デモである」旨は画面上に常時明示する
11. 費用設計
記事の主題の半分は「費用」。この章は実装要件であると同時に、記事本文の原稿素材でもある。
在庫管理だけの特殊事情:ハードウェア費が乗る
他のシステムと違い、在庫管理は現場で物に触る。ソフトだけでは完結しない。
| 層 | 内容 | 特徴 |
|---|---|---|
| ① 初期開発費 | 設計・実装・テスト | 単位換算・ロット・引当の複雑さで大きく変わる |
| ② ハードウェア費 | バーコードリーダー/ハンディ端末/ラベルプリンタ/ラベル用紙/タブレット | 在庫管理固有。台数×人数で積み上がる |
| ③ マスタ整備・データ移行の工数 | SKU整理、単位統一、棚番付与、初期の実地棚卸 | 見積書に「お客様側作業」として明記しないと必ず揉める |
| ④ 固定運用費 | ホスティング、ドメイン、監視、データベース | 小さい |
| ⑤ 変動運用費 | 明細行数課金のSaaS、ラベル用紙の消耗 | 規模次第 |
②③を見積もらずに始めると、「システムはできたが現場で使えない」になる。
②の分岐:ハンディ端末か、スマホか(現場を左右する判断)
| 業務用ハンディ端末 | スマホ + Bluetoothリーダー | スマホのカメラのみ | |
|---|---|---|---|
| 読取速度 | 非常に速い(連射できる) | 速い | 遅い(1件ずつピント合わせ) |
| 連続作業の疲労 | 少ない(片手・トリガー式) | 中 | 多い(画面を見続ける) |
| 落下・粉塵・低温耐性 | 強い | 弱い | 弱い |
| 初期費用 | 高い | 中 | ゼロ(既存端末) |
| 導入の速さ | 遅い(調達・設定) | 中 | 即日 |
判断の目安:
- 1日に数百件スキャンする現場(出荷が多い倉庫)→ ハンディ端末が必須。 カメラでは業務が終わらない
- 1日に数十件(店舗の棚卸、少量出荷)→ スマホ + Bluetoothリーダーで足りる
- まず始めたい/小規模 → カメラのみで開始し、ボトルネックになったら追加
KURA が「キー入力を既定、カメラを任意」にしているのはこの理由(FR-303)。ハンディ端末はキーボードとして振る舞うため、キー入力を主にしておけば、ハード追加時にソフトの改修が不要になる。設計判断が将来のハードウェア選択の自由度を守っている例であり、記事で説明する価値がある。
③の分岐:見落とされやすい「マスタ整備」の工数
在庫管理システムの導入で最も工数を食うのは、実装ではなくマスタ整備。
- SKUの整理(同じ物が別コードで登録されている、廃番が混ざっている)
- 単位の統一(ケース入数がバラバラ、記録がない)
- 棚番の付与(そもそも棚に番号が振られていない)
- 初期在庫の実地棚卸(正しい数を1回作らないと、その後の在庫が全部ずれる)
KURA が初期在庫を「期首棚卸トランザクション」として投入する設計にしているのは(FR-703)、この最初の棚卸を履歴として残し、後から「どこが起点だったか」を追えるようにするため。
①の分岐:既製の在庫管理サービスがあるのに、なぜ自作するのか
この問いに答えられない案件は、受けるべきではない。 正直にそう書くことが信頼を生む。
| 自作が正当化されるケース | 内容 |
|---|---|
| ① 単位換算・ロット規則が独特 | ケース/ボール/バラの三段階、独自ロット採番、規格違い品の同一SKU扱い |
| ② 引当の優先順位ルールがある | 得意先の優先度、出荷期日順、ロット指定出荷 |
| ③ 既存システムと繋ぐ必要がある | 販売管理・EC・会計と在庫を一体化したい |
| ④ 明細行数課金が重い | 出荷明細が多く、SaaSの従量課金が積み上がっている |
| ⑤ 現場の運用に合わせたい | 棚番の体系、検品の手順、承認フローが独特で、既製の画面では回らない |
逆に、①〜⑤のどれにも当てはまらないなら、既製サービスを使うべき。
損益分岐の計算式
既製サービスの年間コスト
= 月額 × 12
+ 明細行数課金(もしあれば)× 年間明細数
+ アドオン費用(拠点追加・ユーザー追加)
回収年数 = (開発費 + ハードウェア費)÷(既製サービスの年間コスト − 自作の年間運用費)
| 規模 | 年間出荷明細 | 判断 |
|---|---|---|
| 小(SKU 100以下・出荷少) | 少 | 既製サービスで十分 |
| 中(SKU 500〜/複数拠点) | 中 | 回収年数を計算する価値あり |
| 大(SKU 数千/独自ルール多数) | 多 | 自作の検討価値が高い |
Webで足りるか、ネイティブが必要か
在庫管理は、Webで足りる領域。
| やりたいこと | Web(PWA) | ネイティブ |
|---|---|---|
| 入出庫・棚卸の入力 | できる | できる |
| ハンディ端末からのキー入力 | できる(キーボード入力として受け取る) | できる |
| カメラでのバーコード読取 | できる(速度は劣る) | 速い |
| オフラインでの入力と後同期 | できる(PWA) | できる |
| 業務用端末の専用SDK(高速スキャナ制御) | できない | できる |
判断の目安: ハンディ端末をキーボードモードで使うなら Webで十分。専用SDKでの高速制御が必要な規模なら、ネイティブ(またはハンディ端末メーカーのアプリ基盤)を検討する。
デモ自体の運用コストを実測して記事に載せる
| ID | 要件 |
|---|---|
| COST-01 | デモ公開後、月次で「デモ起動数・トランザクション登録数・ホスティング費用」を記録すること |
| COST-02 | txn_posted の件数から、「これが実運用だった場合の明細行数課金」を試算して併記すること |
| COST-03 | 記録した実測値を記事に掲載し、確認日を明記すること |
| COST-04 | 既製サービス・ハンディ端末・ラベルプリンタの価格は変動するため、この文書に金額を書き込まない。 記事執筆時点で公式ページを確認し、確認日を明記すること |
12. 実装タスク
この順に進める。フェーズを飛ばさない。 完了時は [x] に更新する。
Phase 0 — 基盤と在庫算出エンジン(4日)
0-1. 初期化
- Next.js 15 / TypeScript strict / App Router / Tailwind で初期化
- ESLint / Prettier / husky(pre-commit で lint + typecheck)
- 9章のディレクトリ構成を作成
0-2. lib/inventory/(心臓部。最初に書いてテストで固める)
-
unit.ts:単位換算(ケース/ボール/バラ ⇔ 最小単位)(FR-110) -
snapshot.ts:4つの在庫数の算出(7章のコードを参照)(FR-102, INV-06) -
ledger.ts:履歴と残高推移、任意時点の在庫(FR-106) -
valuation.ts:移動平均原価と在庫金額(FR-111) -
status.ts:在庫状態の判定 -
fefo.ts:期限順のロット選択(FR-308) -
replenish.ts:判定式「有効在庫 + 入荷予定 < 発注点」と推奨数量(FR-503, FR-504) -
new Date()を内部で呼ばない。現在時刻は引数で受け取る(INV-05) - メモ化と、トランザクション追加時の無効化(INV-04, FR-107)
- 単体テストを網羅的に書く。以下を必ず含めること:
- 引当を作ると、実在庫は変わらず有効在庫だけ減ること(最重要)
- 発注を登録すると入荷予定が増え、入庫すると入荷予定が減って実在庫が増えること
- 拠点間移動で、出荷元から減り「移動中」に計上され、入荷処理で到着先に加わること
- 逆仕訳を追加すると在庫が元に戻り、かつ両方の取引が履歴に残ること(INV-03)
- 単位換算(ケース入庫・バラ出庫)が正しいこと
- FEFO が期限順にロットを選ぶこと
- 移動平均原価が入庫のたびに正しく更新されること
- 任意の過去日の在庫が算出できること
- 推奨発注の判定に入荷予定が反映されること(実在庫だけで判定していないこと)
- 620 SKU × 2拠点 × 5,000トランザクションで 150ms以下をベンチマークで確認(NFR-04)
0-3. lib/analytics/(純粋関数)
- 回転率・滞留日数(FR-603, FR-604)/ABC分析(FR-605)
- 差異集計(原因別・拠点別・担当者別)(FR-609)/欠品・機会損失(FR-608)
- 単体テストを書く
0-4. 型とシード
-
lib/types/に7章の型定義をすべて実装 -
lib/seed/の全ファイル - 商品名を業種の世界観を持つ架空名にする。「商品A」のようなダミーを残さない
- 在庫状態を全帯域に分散させる(欠品/発注点以下/適正/過剰/滞留)
- 引当済がある商品、入荷予定がある商品を必ず作る
- 期限切れ・期限間近のロット、移動中在庫、遅延発注、未承認の棚卸差異を作る
- トランザクションの時系列に矛盾がないこと(在庫がマイナスになる履歴を作らない)
- 日付は現在日時からの相対で生成する
0-5. ストアとリポジトリ
-
lib/store/{session,data,settings,sync,ui}.tsを Zustand + persist で実装 -
lib/repo/_scope.tsを最初に実装する(データスコープ + 項目スコープ)(SCP-01〜03, FR-707, FR-708) -
lib/repo/の全モジュール。全メソッド async、Result型、スコープ適用 - 在庫を変えるのはトランザクション追加のみ(INV-02)
- スコープと項目スコープの単体テストをこの時点で書く(原価が現場担当のデータに含まれないこと)
- 在庫の再算出と絞り込みに擬似ディレイを入れない
0-6. デザインシステム
-
app/globals.cssにカラートークンを CSS 変数で定義(在庫状態色 + 水位バーの構成色) -
tailwind.config.tsから CSS 変数を参照するよう theme を拡張 - フォント(Noto Sans JP / Roboto Condensed)を
next/fontで最適化。数値にtabular-nums - shadcn/ui を導入し、トークンに合わせて上書き
- アプリシェル(サイドバー折りたたみ、ヘッダー、パンくず)
-
StockGauge.tsx:在庫水位バー(行内120px / 詳細フル幅の2サイズ) -
FourNumbers.tsx:4つの在庫数ブロック(− と = で関係を示す) - バーの
aria-labelと数値の併記(A11Y-05, A11Y-06) - 共通コンポーネント:
StockStatusBadge/ExpiryBadge/MetricCard/EmptyState(2種の出し分け)/Skeleton(水位バーの位置も確保) -
prefers-reduced-motionの対応 -
/dev/componentsにコンポーネントカタログを作成
0-7. デモ基盤
-
RoleSwitcher(3ロール + 現場担当の拠点選択)(FR-706) - デモ切替バーを固定配置。プロダクトのトークンとは別扱いのデザイン
- 「デモをリセット」(FR-709)/ロール権限外アクセス時の案内画面(FR-711)
- 元記事リンク・資料ダウンロード(FR-713)
Phase 0 完了チェック
-
lib/inventory/のテストが全パターン通る - 「引当で有効在庫だけが減る」がテストで検証済み
- 「逆仕訳で在庫が戻り、履歴が消えない」がテストで検証済み
- ロールを切り替えると、リポジトリが返す件数と項目(原価の有無)が変わる
- 620 SKU の在庫算出が150ms以下
-
/dev/componentsで水位バーが全パターン正しく描画される
Phase 1 — 一覧基盤と在庫照会(4日)
1-1. データテーブル基盤(ここの品質が全画面に波及する)
-
lib/query/に絞り込み・ソート・ビュー適用ロジックを実装(テスト付き) -
DataTable(TanStack Table ヘッドレス + 自前UI)。固定ヘッダー、列幅リサイズ - 列カスタマイズ(FR-202)/複数列ソート
-
FilterBar:項目ごとの条件指定(FR-203) - 在庫状態での絞り込み(FR-204)
- URLクエリとの同期(FR-205)/
ConditionChips(FR-206) - 行選択と
BulkActionBar(下からせり上がる)(FR-209, FR-210) - 取り消し可能な操作トースト(IX-06)
- 仮想スクロール(FR-213)/密度切替(FR-214)
- キーボード操作(
↑↓EnterSpace⌘A)(IX-02) - 数量列の右寄せ + Condensed 表示(FR-215)
1-2. 保存ビュー
-
ViewSidebar:ビュー一覧、件数バッジ、ワンクリック切替(FR-208) - ビューの保存・編集・削除。個人/共有の区別(FR-207)
1-3. 在庫照会
- SC-010 在庫一覧(4つの在庫数 + 行内水位バー)(FR-201)
- SC-011 商品詳細(4数値ブロック + フル幅水位バー + 拠点別・ロット別内訳)
- SC-012 在庫履歴(元帳)。残高列で「積み上げ」を可視化(FR-101, FR-106)
- SC-013 商品の登録・編集
- SC-020 拠点・ロケーション/SC-021 取引先
- SC-001 ダッシュボード(ロールで表示内容が変わる)(FR-601, FR-602)
1-4. CSV入出力・検索
-
lib/csv/:インポート(列マッピング・検証)/エクスポート - SC-014 インポート4ステップ(FR-701, FR-702)
- 初期在庫を「期首棚卸トランザクション」として登録(FR-703)
- 数式インジェクションの無害化(FR-704, NFR-12)
- エクスポート(FR-705)
- グローバル検索(SKU・商品名・JAN・ロット番号)(FR-211)/
⌘Kコマンドパレット(FR-212)
1-5. 埋め込みモード
- SC-900 埋め込みモード:1商品の水位バー + 入出庫ボタン(FR-715, 2章)
- 引当ボタンで有効在庫だけが減ることが見えること
- カメラを呼ばない(EMB-04)/アプリ内遷移をしない(EMB-05)
- 初期バンドル 60KB以下を実測(NFR-08)
Phase 1 完了チェック
- フローAが完走する(埋め込みで4つの在庫数が別々に動く → 全画面 → 履歴で積み上げが追える)
- 620行でも絞り込みが100ms以下、スクロールが60fps
- テーブルがキーボードのみで操作完遂できる
- 現場担当ロールで原価・在庫金額の列が存在しない(CSSで隠していない)
Phase 2 — 入出庫・移動・スキャン(4日)
2-1. スキャン入力基盤
-
ScanInput.tsx:常時フォーカスを保つ入力欄。スキャン後もフォーカスを失わない(IX-03) -
ScanLineList.tsx:スキャンごとに行が積み上がる。同一SKUは数量 +1(FR-302) - 各行の取り消しボタン
- スキャンの成功・失敗を
aria-liveで通知(A11Y-10) - 1件あたり50ms以下の入力遅延を実測(NFR-07)
-
CameraScanner.tsx(@zxing/browser、動的import・任意で有効化)(FR-303) - 未登録バーコードの扱い(その場で登録/保留)(FR-304)
2-2. 入庫・出庫
- SC-100 入庫(検品):発注選択、数量差異、部分入荷(FR-301)
- ロット管理対象では、ロット番号と期限の入力を必須化(FR-305)
- SC-101 棚入れ(既存棚の推奨)(FR-306)
- SC-102 出庫(ピッキング):巡回順のリスト(FR-307)
- FEFO推奨と逸脱時の理由確認(FR-308)
- 有効在庫超過の警告(ブロックはしない)(FR-309)
- SC-103 ピッキングリスト印刷(PDF、動的import)
- SC-104 引当:実在庫を変えずに有効在庫を減らす(FR-310)
2-3. 移動・調整・取消
- SC-110 在庫移動:拠点間は「移動中」状態を持つ(FR-311)
- SC-111 移動の入荷処理/移動中一覧と滞留日数(FR-312)/棚間移動(FR-313)
- SC-120 在庫調整:理由コード必須(FR-314)
- SC-121 取消(逆仕訳の追加)。元取引に「取消済」の印を付け、履歴から消さない(FR-315)
- SC-130 トランザクション一覧(絞り込み、CSV出力)
- 全取引に担当者・拠点・日時・端末種別を記録(FR-317)
Phase 2 完了チェック
- フローCの後半が完走する(発注 → 入庫 → 棚入れ → 期限アラート → FEFO出庫)
- 4章「ロールを跨ぐ体験」の1・4・6が動作する
- 連続スキャンが止まらず、フォーカスが外れない
- 取消後も両方の取引が履歴に残っている
Phase 3 — 棚卸(3日・1章の事実①を体験に落とす)
- SC-201 棚卸の開始:対象範囲の指定、理論在庫の凍結(FR-401)
- 棚卸中も通常の入出庫を継続でき、凍結後の取引を差異計算から除外(FR-402)
- SC-202 カウント入力:棚番順リスト、バーコード、1画面で連続入力(FR-403, IX-09)
- 未カウント件数・進捗率の常時表示(FR-404)
- SC-203 差異一覧:理論・実数・差異数・差異金額・差異率ソート(FR-405)
- 原因分類を必須化。未分類があると承認に進めない(FR-406)
- SC-204 承認と確定:承認するまで在庫を動かさない。承認時に調整トランザクションを生成(FR-408)
- 承認権限の制御(カウント者の単独承認を防ぐ設定)(FR-409)
- SC-200 棚卸一覧/SC-205 棚卸履歴(差異推移)(FR-410)
Phase 3 完了チェック
- フローBが完走する(棚卸開始 → カウント → 差異 → 原因分類 → 承認 → 在庫が動く → 履歴に残る)
- 4章「ロールを跨ぐ体験」の2が動作する
- 承認前に在庫が動いていないことを確認
Phase 4 — 発注・期限・分析(3日)
4-1. 発注・期限
- 発注点・安全在庫・発注ロットの設定(FR-501)
- 発注点の推奨値の算出(リードタイム × 平均出庫 + 安全在庫)(FR-502)
- SC-300 推奨発注リスト:判定式に入荷予定を含める(FR-503, FR-504)
- 仕入先別グループ化と最小発注金額の表示(FR-505)
- SC-301 発注書の作成・PDF出力(FR-506)
- SC-302 発注一覧・入荷予定・遅延アラート(FR-507)
- SC-310 期限アラート:閾値設定、期限切れの扱い(FR-509, FR-510)/SC-311 ロット一覧
- 期限切れの廃棄登録(FR-511)
4-2. 分析・設定
- SC-400 在庫回転率・滞留(FR-603, FR-604)
- SC-401 ABC分析(パレート図)(FR-605)/SC-402 在庫金額推移(FR-607)
- SC-403 欠品分析(FR-608)/SC-404 差異分析(原因別・拠点別・担当者別)(FR-609)
- 全分析画面の期間指定(FR-610)
- SC-500 設定(理由コード・単位・期限閾値・発注点の一括設定)(FR-611)
- SC-501 ユーザー・権限(FR-612)/SC-502 操作ログ(FR-613)
Phase 4 完了チェック
- 4章「ロールを跨ぐ体験」の3と5が動作する
- 差異分析に意味のあるパターンが出ている(シードの品質確認)
- 推奨発注リストが「入荷予定込み」で判定されている
Phase 5 — オフライン・記事統合・仕上げ(3日)
5-1. オフライン
- Service Worker / PWA マニフェスト/オフライン検知とバナー
- オフラインでの入出庫記録と未同期バッジ(FR-316)
- オンライン復帰時の自動同期と進捗表示
- localStorage 上限時の圧縮処理(NFR-13)
5-2. 記事統合
- 記事側の埋め込みコードを作成し、実際の記事ページで動作確認(EMB-01〜05)
- 記事の Core Web Vitals を埋め込み前後で比較検証
- 「全画面で試す」カードの設置
- 資料PDF(在庫要件チェックシート・損益分岐計算シート)の作成とダウンロード導線
- 元記事リンク・問い合わせ導線
- GA4 イベント設定(
txn_posted/allocation_created/stocktake_done/csv_imported等)(EMB-06) - GA4 に商品名・取引先名を送っていないことを確認(EMB-07, NFR-14)
-
?embed=1をnoindexに、デモ本体の title / description / OGP(EMB-08)
5-3. 体験の総点検
- 4章「ロールを跨ぐ体験」の6パターンをすべて手動で確認
- 全32画面を開き、空の画面が1つもないことを確認
- 空状態(データ0件/絞り込み0件)の出し分けを確認
- ガイドツアーを実装(FR-714)
5-4. アクセシビリティ監査
- axe-core を全画面に実行し、Critical / Serious を0件に
- 13px本文のコントラスト比を実測(A11Y-01)
- 水位バーの
aria-labelと数値併記を確認(A11Y-05, A11Y-06) - キーボードのみで全画面・スキャン入力・棚卸を完遂
- フォントサイズ200%での確認(A11Y-12)
5-5. パフォーマンス
- PDF・グラフ・カメラを動的import に切り出し、初期バンドルを250KB以下に(NFR-09)
- 在庫算出150ms以下、絞り込み100ms以下、スキャン50ms以下を実測(NFR-04, NFR-05, NFR-07)
- 620行のスクロール60fpsを実測(NFR-06)
- Lighthouse で Performance 90 / Accessibility 95(NFR-10)
5-6. 公開と記録
- Playwright で フローA・B・C の E2E
- Vitest:
lib/inventorylib/analyticslib/querylib/csv_scopeのカバレッジ85%以上 -
/dev/componentsを本番で非公開に - Vercel へデプロイ
- トランザクション登録数の記録を開始。明細行数課金の換算も試算(COST-01, COST-02)
- 解説動画(3分)の収録:4つの在庫数 → 引当で有効在庫だけ減る → 棚卸で差異 → 原因分類 → 承認で在庫が動く → 履歴に残る
- 各フェーズのキャプチャを整理し、記事の「方法」パートの素材にまとめる
合計 21営業日(約4週間)/1名専任 + レビュー体制。
工数配分の意図: Phase 0 に4日、うち
lib/inventory/のテストに2日を充てる。在庫算出はこのプロジェクトの価値そのものであり、かつ間違っていても画面上は動いているように見える種類のバグを生む。「引当で有効在庫だけが減る」「逆仕訳で在庫が戻り履歴が残る」がテストで保証されていない状態で画面を作り始めると、後半で全画面の数字を疑うことになる。 Phase 1 に4日を割いているのは、データテーブル基盤が全一覧画面に波及するため。逆にここが良ければ、トランザクション・ロット・発注の一覧はほぼ設定だけで完成する。
13. 受入基準
- 6章の優先度 P1 の要件がすべて実装され、テストで合格していること
- 5章の主要フローA・B・Cが、エンドツーエンドで完走すること
- 4章「ロールを跨ぐ体験」の6パターンがすべて動作すること
- 在庫数量がストアに保存されておらず、常にトランザクションから算出されていること
- 在庫を変更するコードが、トランザクションの追加以外に存在しないこと
- 4つの在庫数が同一の関数から返され、在庫一覧に同時表示されていること
- 引当を作成すると、実在庫は変わらず有効在庫のみが減ること
- 取消が逆仕訳として実装され、元の取引が履歴から消えないこと
- 拠点間移動で「移動中」状態が存在し、入荷処理まで到着先の在庫に入らないこと
- 推奨発注リストの判定に入荷予定が含まれていること(実在庫だけで判定していない)
- 棚卸の差異が、原因分類と承認を経てから調整トランザクションとして登録されること。承認前に在庫が動かないこと
- ロールを切り替えると、見える行(拠点)と見える列(原価・在庫金額)の両方が変わること。かつ原価がデータに含まれずに返されていること(CSSで隠していない)
- 620 SKU × 5,000トランザクションで、在庫算出が150ms以下、絞り込みが100ms以下であること
- 連続スキャンで入力欄のフォーカスが外れず、1件あたり50ms以下で処理されること
- 水位バーが
aria-labelを持ち、4つの在庫数が数値としても表示されていること - CSVインポートが列マッピングとエラー行表示に対応し、初期在庫が期首棚卸トランザクションとして登録されること
- CSVエクスポートと発注書PDFが実際にダウンロードできること
- 全32画面のいずれにも空の状態がなく、リアルなデータが表示されていること
- リロード後も操作結果が保持され、「デモをリセット」で初期状態に戻ること
- axe-core で Critical / Serious の指摘が0件であること
- データアクセスがすべて
lib/repo/を経由し、在庫算出がlib/inventory/の外に存在しないこと - RELATE と並べたときに、明確に別のプロダクトとして見えること(水位バーと Condensed 数値が効いていること)
判断に迷ったときのルール
- 仕様がこの文書にない場合は、実装せずに質問する。 推測で作らない
- 在庫数を書き換えない。 出来事(トランザクション)を追加する。これがこのプロジェクトの存在理由
- 在庫算出を
lib/inventory/の外に書かない。「ここだけ簡単に足せば」が全ての崩壊の始まり lib/inventory/とlib/analytics/を純粋関数に保つ。 現在時刻は必ず引数で受け取る- トランザクションを削除・編集しない。 取消は逆仕訳
- 4つの在庫数を混同しない。 画面でも文言でも、必ず区別して表示する
- 原価・在庫金額は権限がなければデータに含めない。 CSSで隠すのは実装ミス
- 警告はブロックしない。 現場は理屈通りに動かない。警告して記録し、あとで直せるようにする
- スキャン入力のフォーカスを失わせない。 ここが崩れると現場で使われなくなる
- 数量は常に最小単位・符号付きで保持する。 表示時に換算する
- スコープ外(3章 Won't have)は実装しない。特に単位換算・引当優先順位・セット品は要件定義段階で洗い出す
- 「デモだから」を理由に品質を落とす判断はしない
- RELATE のトークン・コンポーネントをコピーしない。 同じ業務システムでも方向は別

