feat: [재고] 적정재고 관리 기능 추가 (max_stock + over 상태 + update API)

This commit is contained in:
김보곤
2026-03-21 07:59:53 +09:00
parent 5860262d87
commit 244a1f7a24
5 changed files with 94 additions and 0 deletions

View File

@@ -73,6 +73,32 @@ public function statsByItemType(): JsonResponse
return ApiResponse::success($stats, __('message.fetched'));
}
/**
* 재고 수정 (안전재고, 최대재고, 사용상태)
*/
public function update(int $id, Request $request): JsonResponse
{
try {
$data = $request->validate([
'safety_stock' => 'nullable|numeric|min:0',
'max_stock' => 'nullable|numeric|min:0',
'is_active' => 'nullable|boolean',
]);
// 최대재고가 설정된 경우 안전재고 이상이어야 함
if (isset($data['max_stock']) && $data['max_stock'] > 0
&& isset($data['safety_stock']) && $data['safety_stock'] > $data['max_stock']) {
return ApiResponse::error('최대재고는 안전재고 이상이어야 합니다.', 422);
}
$stock = $this->service->updateStock($id, $data);
return ApiResponse::success($stock, __('message.updated'));
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
return ApiResponse::error(__('error.stock.not_found'), 404);
}
}
/**
* 재고 조정 이력 조회
*/

View File

@@ -23,6 +23,7 @@ class Stock extends Model
'unit',
'stock_qty',
'safety_stock',
'max_stock',
'reserved_qty',
'available_qty',
'lot_count',
@@ -39,6 +40,7 @@ class Stock extends Model
protected $casts = [
'stock_qty' => 'decimal:3',
'safety_stock' => 'decimal:3',
'max_stock' => 'decimal:3',
'reserved_qty' => 'decimal:3',
'available_qty' => 'decimal:3',
'lot_count' => 'integer',
@@ -65,6 +67,7 @@ class Stock extends Model
'normal' => '정상',
'low' => '부족',
'out' => '없음',
'over' => '초과',
];
/**
@@ -140,6 +143,10 @@ public function calculateStatus(): string
return 'low';
}
if ($this->max_stock > 0 && $this->stock_qty > $this->max_stock) {
return 'over';
}
return 'normal';
}

View File

@@ -191,6 +191,36 @@ public function show(int $id): Item
->findOrFail($id);
}
/**
* 재고 수정 (안전재고, 최대재고, 사용상태)
*/
public function updateStock(int $id, array $data): Item
{
$tenantId = $this->tenantId();
$item = Item::where('tenant_id', $tenantId)->findOrFail($id);
$stock = $item->stock;
if ($stock) {
if (isset($data['safety_stock'])) {
$stock->safety_stock = $data['safety_stock'];
}
if (isset($data['max_stock'])) {
$stock->max_stock = $data['max_stock'];
}
$stock->status = $stock->calculateStatus();
$stock->updated_by = $this->apiUserId();
$stock->save();
}
if (isset($data['is_active'])) {
$item->is_active = $data['is_active'];
$item->save();
}
return $item->load(['stock.lots' => fn ($q) => $q->orderBy('fifo_order')]);
}
/**
* 재고 조정 이력 조회
*/

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('stocks', function (Blueprint $table) {
$table->decimal('max_stock', 15, 3)->default(0)
->comment('최대 재고 (적정재고 상한)')
->after('safety_stock');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('stocks', function (Blueprint $table) {
$table->dropColumn('max_stock');
});
}
};

View File

@@ -109,6 +109,7 @@
Route::get('/stats', [StockController::class, 'stats'])->name('v1.stocks.stats');
Route::get('/stats-by-type', [StockController::class, 'statsByItemType'])->name('v1.stocks.stats-by-type');
Route::get('/{id}', [StockController::class, 'show'])->whereNumber('id')->name('v1.stocks.show');
Route::put('/{id}', [StockController::class, 'update'])->whereNumber('id')->name('v1.stocks.update');
Route::get('/{id}/adjustments', [StockController::class, 'adjustments'])->whereNumber('id')->name('v1.stocks.adjustments');
Route::post('/{id}/adjustments', [StockController::class, 'storeAdjustment'])->whereNumber('id')->name('v1.stocks.adjustments.store');
});