From 244a1f7a24f3bb28529636b41e58411eef131695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sat, 21 Mar 2026 07:59:53 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[=EC=9E=AC=EA=B3=A0]=20=EC=A0=81?= =?UTF-8?q?=EC=A0=95=EC=9E=AC=EA=B3=A0=20=EA=B4=80=EB=A6=AC=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(max=5Fstock=20+=20over=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20+=20update=20API)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Api/V1/StockController.php | 26 ++++++++++++++++ app/Models/Tenants/Stock.php | 7 +++++ app/Services/StockService.php | 30 +++++++++++++++++++ ...0_162822_add_max_stock_to_stocks_table.php | 30 +++++++++++++++++++ routes/api/v1/inventory.php | 1 + 5 files changed, 94 insertions(+) create mode 100644 database/migrations/2026_03_20_162822_add_max_stock_to_stocks_table.php diff --git a/app/Http/Controllers/Api/V1/StockController.php b/app/Http/Controllers/Api/V1/StockController.php index c19e6eb8..4d2eca28 100644 --- a/app/Http/Controllers/Api/V1/StockController.php +++ b/app/Http/Controllers/Api/V1/StockController.php @@ -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); + } + } + /** * 재고 조정 이력 조회 */ diff --git a/app/Models/Tenants/Stock.php b/app/Models/Tenants/Stock.php index 0b734079..8ff2720c 100644 --- a/app/Models/Tenants/Stock.php +++ b/app/Models/Tenants/Stock.php @@ -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'; } diff --git a/app/Services/StockService.php b/app/Services/StockService.php index 65820ed6..d3ff99e6 100644 --- a/app/Services/StockService.php +++ b/app/Services/StockService.php @@ -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')]); + } + /** * 재고 조정 이력 조회 */ diff --git a/database/migrations/2026_03_20_162822_add_max_stock_to_stocks_table.php b/database/migrations/2026_03_20_162822_add_max_stock_to_stocks_table.php new file mode 100644 index 00000000..8ca99d7c --- /dev/null +++ b/database/migrations/2026_03_20_162822_add_max_stock_to_stocks_table.php @@ -0,0 +1,30 @@ +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'); + }); + } +}; diff --git a/routes/api/v1/inventory.php b/routes/api/v1/inventory.php index 9dd8e624..e689107c 100644 --- a/routes/api/v1/inventory.php +++ b/routes/api/v1/inventory.php @@ -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'); });