本日1件のお問い合わせがありました。

✉️FIELDにお問い合わせ

システム開発

ICタグを用いてスマフォをかざしてワインの在庫管理をするシステムの設計

NFCタグを活用したワイン在庫管理システム(倉庫・店舗対応版)の設計を書いてみます。

倉庫が複数あり、出荷先の店舗も管理する形を想定してみましょう。
このシステムでは 「どの倉庫にどのワインが何本あるか」「どの倉庫からどの店舗へ出荷されたか」 を記録することを目的としています。


1. URLツリーと画面

URLツリー

/login                         - ログイン画面
/dashboard - ダッシュボード
/wines - ワイン一覧
/wines/create - ワイン登録
/wines/{wine_id} - ワイン詳細
/wines/{wine_id}/edit - ワイン編集
/bottles/register - NFCタグ登録(倉庫内在庫追加)
/bottles/ship - NFCタグ出荷(倉庫から店舗へ)
/bottles/{tag_id} - ボトル詳細(NFCタグ読み取り後)
/transactions - 在庫変更履歴(追加/出荷)
/warehouses - 倉庫一覧
/warehouses/create - 倉庫登録
/stores - 店舗一覧
/stores/create - 店舗登録
/settings - 設定画面(管理者用)

2. 業務フローと画面紹介

📌 1. ログイン (/login)

  • 管理者はメールアドレスとパスワードでログイン。
  • 認証成功後、ダッシュボードへ遷移。

📌 2. ダッシュボード (/dashboard)

  • 全倉庫のワイン在庫サマリを表示(倉庫ごとの在庫数、最新出荷記録など)。
  • 倉庫・ワイン管理の各画面へのリンクを提供。

📌 3. ワイン登録 (/wines/create)

  • 新しいワインを登録(ワイン名、ヴィンテージ、産地、価格など)。
  • 登録後、「倉庫に在庫を追加」ボタンをクリックし、NFCタグ登録へ。

📌 4. 倉庫登録 (/warehouses/create)

  • 倉庫の基本情報を登録(倉庫名、住所など)。
  • 各倉庫ごとのワイン在庫を管理。

📌 5. 店舗登録 (/stores/create)

  • 店舗の基本情報を登録(店舗名、住所など)。
  • 出荷時に出荷先店舗を選択。

📌 6. NFCタグ登録 (/bottles/register)

  • ワインのNFCタグをスキャン → 倉庫を選択し在庫に追加
  • システムがタグのIDを読み取り、bottles テーブルに保存。

📌 7. 在庫一覧 (/warehouses)

  • 各倉庫のワイン在庫状況を確認。

📌 8. NFCタグ読み取り (/bottles/{tag_id})

  • NFCタグをスマホで読み取ると、該当ワイン情報と倉庫内在庫状況を表示。

📌 9. 出荷 (/bottles/ship)

  • NFCタグをスキャンし、「どの倉庫からどの店舗へ出荷するか」を選択。
  • システムがbottlesstatusshipped に更新し、transactions テーブルに記録。

📌 10. 在庫変更履歴 (/transactions)

  • 在庫追加・出荷の履歴を一覧表示(倉庫ごと・店舗ごとでフィルタ可能)。

3. データベース設計(倉庫・店舗対応)

users(管理者情報)

ColumnTypeDescription
idBIGINT (PK)ユーザーID
nameVARCHAR(255)名前
emailVARCHAR(255)メールアドレス
passwordVARCHAR(255)パスワード(ハッシュ化)
created_atTIMESTAMP登録日時
updated_atTIMESTAMP更新日時

wines(ワイン情報)

ColumnTypeDescription
wine_idBIGINT (PK)ワインID
nameVARCHAR(255)ワイン名
vintageINTヴィンテージ(年)
regionVARCHAR(255)産地
priceDECIMAL(10,2)価格
created_atTIMESTAMP登録日時
updated_atTIMESTAMP更新日時

warehouses(倉庫情報)

ColumnTypeDescription
warehouse_idBIGINT (PK)倉庫ID
nameVARCHAR(255)倉庫名
locationTEXT住所
created_atTIMESTAMP登録日時
updated_atTIMESTAMP更新日時

stores(店舗情報)

ColumnTypeDescription
store_idBIGINT (PK)店舗ID
nameVARCHAR(255)店舗名
locationTEXT住所
created_atTIMESTAMP登録日時
updated_atTIMESTAMP更新日時

bottles(ボトル/NFCタグ情報)

ColumnTypeDescription
tag_idVARCHAR(64) (PK)NFCタグの識別子
wine_idBIGINT (FK)紐付けるワインのID(外部キー)
warehouse_idBIGINT (FK)現在の倉庫ID(出荷時にNULLになる)
statusENUMin_stock(在庫)or shipped(出荷済)
added_atTIMESTAMP在庫追加日時
shipped_atTIMESTAMP出荷日時

transactions(在庫履歴)

ColumnTypeDescription
transaction_idBIGINT (PK)取引ID
tag_idVARCHAR(64) (FK)対象ボトル(外部キー)
wine_idBIGINT (FK)関連ワインのID(外部キー)
warehouse_idBIGINT (FK)出荷元の倉庫ID
store_idBIGINT (FK)出荷先の店舗ID
actionENUMADD(追加)or SHIP(出荷)
timestampTIMESTAMP実行日時
user_idBIGINT (FK)実行した管理者(外部キー)

4. 業務フロー

  1. ワインを登録/wines/create
  2. 倉庫にNFCタグを登録/bottles/register
  3. 倉庫ごとに在庫を管理/warehouses
  4. 出荷時にNFCタグをスキャンし、倉庫→店舗へ移動/bottles/ship
  5. 在庫変更履歴を記録/transactions

業務を詳しくてみていきます。

1. ワインを登録(/wines/create)

📌 人の動き

  • 管理者はワインの基本情報(名前、ヴィンテージ、産地、価格)をシステムに登録する。
  • ワイン情報を登録した後、そのワインのNFCタグを倉庫に紐付ける必要がある。

2. 倉庫にNFCタグを登録(/bottles/register)

📌 人の動き

  • 管理者は登録済みのワインにNFCタグを紐付けるため、スマホでタグをスキャン。
  • 画面で倉庫を選択し、「在庫に追加」ボタンをクリックする。

3. 倉庫ごとに在庫を管理(/warehouses)

📌 人の動き

  • 管理者は各倉庫にあるワインの在庫を一覧で確認。
  • 必要があれば在庫の移動や追加を実施。

4. 出荷時にNFCタグをスキャン(/bottles/ship)

📌 人の動き

  • 管理者は出荷するワインのNFCタグをスマホでスキャン。
  • 画面で出荷元の倉庫と配送先の店舗を選択。
  • 「出荷する」ボタンを押すと在庫が更新される。

5. 在庫変更履歴を記録(/transactions)

📌 人の動き

  • 管理者は在庫の追加・出荷履歴を確認。
  • 出荷履歴をフィルタして、倉庫ごとの動きをチェック。

ラベルにスマフォをかざして在庫をプラスする時のapiとUI

1. Laravel(APIサーバー)

📍 routes/api.php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\InventoryController;

Route::post('/scan-nfc', [InventoryController::class, 'registerOrUpdate']);

📍 app/Http/Controllers/InventoryController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Bottle;
use App\Models\Wine;
use App\Models\Transaction;
use App\Models\Warehouse;
use Illuminate\Support\Facades\DB;

class InventoryController extends Controller
{
public function registerOrUpdate(Request $request)
{
$tagId = $request->input('tag_id');
$warehouseId = $request->input('warehouse_id'); // どの倉庫か
$wineId = $request->input('wine_id'); // どのワインか

if (!$tagId || !$warehouseId || !$wineId) {
return response()->json(['error' => 'Invalid data'], 400);
}

return DB::transaction(function () use ($tagId, $warehouseId, $wineId) {
// 既に登録されたボトルか確認
$bottle = Bottle::where('tag_id', $tagId)->first();

if (!$bottle) {
// 新規登録
$bottle = Bottle::create([
'tag_id' => $tagId,
'wine_id' => $wineId,
'warehouse_id' => $warehouseId,
'status' => 'in_stock',
'added_at' => now(),
]);

// 在庫を+1
Wine::where('wine_id', $wineId)->increment('stock_count');

// 在庫追加履歴を記録
Transaction::create([
'tag_id' => $tagId,
'wine_id' => $wineId,
'warehouse_id' => $warehouseId,
'action' => 'ADD',
'timestamp' => now(),
]);

return response()->json(['message' => 'New bottle registered and stock increased']);
}

return response()->json(['message' => 'Tag already exists, no change'], 200);
});
}
}

📍 app/Models/Bottle.php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Bottle extends Model
{
use HasFactory;

protected $fillable = [
'tag_id',
'wine_id',
'warehouse_id',
'status',
'added_at',
];

public $timestamps = false;
}

📍 app/Models/Wine.php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Wine extends Model
{
use HasFactory;

protected $fillable = [
'wine_id',
'name',
'vintage',
'region',
'price',
'stock_count',
];
}

📍 app/Models/Transaction.php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Transaction extends Model
{
use HasFactory;

protected $fillable = [
'tag_id',
'wine_id',
'warehouse_id',
'action',
'timestamp',
];
}

2. Next.js(NFCタグスキャン + API連携)

📍 components/NFCScanner.tsx

import { useState, useEffect } from 'react';

const NFCScanner = () => {
const [tagId, setTagId] = useState<string | null>(null);
const [message, setMessage] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);

useEffect(() => {
if ("NDEFReader" in window) {
const nfcReader = new NDEFReader();
nfcReader.scan().then(() => {
nfcReader.onreading = (event) => {
const decoder = new TextDecoder();
for (const record of event.message.records) {
const tagData = decoder.decode(record.data);
setTagId(tagData);
handleRegister(tagData);
}
};
}).catch((error) => {
console.error("NFC読み取りエラー:", error);
});
}
}, []);

const handleRegister = async (tagId: string) => {
setLoading(true);
try {
const response = await fetch("/api/register-nfc", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
tag_id: tagId,
warehouse_id: 1, // 適当な倉庫ID
wine_id: 1 // 適当なワインID
})
});

const data = await response.json();
setMessage(data.message);
} catch (error) {
setMessage("エラーが発生しました");
console.error(error);
} finally {
setLoading(false);
}
};

return (
<div className="container">
<h2>NFCスキャン</h2>
<p>タグをスマホにかざしてください</p>
{loading ? <p>読み取り中...</p> : <p>{message}</p>}
</div>
);
};

export default NFCScanner;

📍 pages/api/register-nfc.ts

import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method Not Allowed' });
}

try {
const { tag_id, warehouse_id, wine_id } = req.body;

if (!tag_id || !warehouse_id || !wine_id) {
return res.status(400).json({ error: 'Invalid request' });
}

const apiResponse = await fetch(process.env.LARAVEL_API_URL + '/api/scan-nfc', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ tag_id, warehouse_id, wine_id })
});

const data = await apiResponse.json();
return res.status(apiResponse.status).json(data);
} catch (error) {
return res.status(500).json({ error: 'Server error' });
}
}

実装の流れ

  1. 管理者がスマホでNFCタグをかざす
  2. Next.jsのNFCScannerコンポーネントがタグのIDを取得
  3. Next.jsがLaravelのAPI /api/scan-nfc にリクエストを送る
  4. Laravelがデータベースをチェック
    • 未登録のタグなら → 新規登録 & 在庫を+1
    • 既に登録済みなら → 何もしない
  5. レスポンスをNext.jsに返し、画面に結果を表示

Yamamoto Yuya

プロフェッショナルとしての高いスキルと知識を持ち、誠実さと責任感を大切にする。常に向上心を持ち、新たな挑戦にも積極的に取り組む努力家。

  • laravel11を使ってwebサービス開発するための入門解説

    目次これを見て自分もで基本的な流れを解説しながらマッチングアプリを作ってみようかなと思いましたまずは

    • #laravel
  • はじめてのLaravel10開発入門[環境構築から実装まで解説]

    こんにちは月岡ですですがの方はこちら今我々はちょっくらバンコクでも旅行しながら仕事でもしててくれとい

    • #laravel
  • オフショアの品質がやばいのはN+1問題があるからです←こうやって解決する

    オフショア関係なく事業部側が制作側にどういう指示を出したらうまく作ってくれるんだろうって思うことあり

    • #laravel
  • php8.1からひょこっと現れたenumはlaravelで必須ですよ

    例えばもう大体が出来上がっているときに自分がお気に入りをした求人の一覧ペジを作るということを例にを説

    • #laravel
  • Laravelとhorizon-supervisorの解説(redis落ちたら終わり^^b)

    のキュにを入れる方法を記載していきますの作成と実行の流れまずコンテナに入りを実行しましょうそうすると

    • #laravel
    • #redis
    • #horizon
  • Laravelのapiの作り方とserviceとrepositoryの使い方とか5分で学べる。

    これを知りたいという人多いですよねこれまでのブログでの簡単な扱い方としてのやの読み込みを使ったやその

    • #laravel