only([ 'search', 'item_type', 'item_category', 'status', 'location', 'sort_by', 'sort_dir', 'per_page', 'page', 'start_date', 'end_date', ]); $stocks = $this->service->index($params); return ApiResponse::success($stocks, __('message.fetched')); } /** * 재고 통계 조회 */ public function stats(): JsonResponse { $stats = $this->service->stats(); return ApiResponse::success($stats, __('message.fetched')); } /** * 재고 상세 조회 (LOT 포함) */ public function show(int $id): JsonResponse { try { $stock = $this->service->show($id); return ApiResponse::success($stock, __('message.fetched')); } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { return ApiResponse::error(__('error.stock.not_found'), 404); } } /** * 품목유형별 통계 조회 */ public function statsByItemType(): JsonResponse { $stats = $this->service->statsByItemType(); 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); } } /** * 재고 조정 이력 조회 */ public function adjustments(int $id): JsonResponse { try { $adjustments = $this->service->adjustments($id); return ApiResponse::success($adjustments, __('message.fetched')); } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { return ApiResponse::error(__('error.stock.not_found'), 404); } } /** * 재고 조정 등록 */ public function storeAdjustment(int $id, StoreStockAdjustmentRequest $request): JsonResponse { try { $result = $this->service->createAdjustment($id, $request->validated()); return ApiResponse::success($result, __('message.created')); } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { return ApiResponse::error(__('error.stock.not_found'), 404); } } /** * 재고 거래이력 (사용현황) * GET /api/v1/stocks/{id}/transactions */ public function transactions(int $id): JsonResponse { $tenantId = app('tenant_id'); $stock = DB::table('stocks') ->where('tenant_id', $tenantId) ->where('item_id', $id) ->first(); if (! $stock) { // item_id로 직접 검색 $stock = DB::table('stocks') ->where('tenant_id', $tenantId) ->where('id', $id) ->first(); } $stockId = $stock?->id; $itemCode = $stock?->item_code; $transactions = DB::table('stock_transactions') ->where('tenant_id', $tenantId) ->where(function ($q) use ($stockId, $itemCode) { if ($stockId) { $q->where('stock_id', $stockId); } if ($itemCode) { $q->orWhere('item_code', $itemCode); } }) ->orderByDesc('created_at') ->limit(100) ->get([ 'id', 'type', 'qty', 'balance_qty', 'reference_type', 'reference_id', 'lot_no', 'reason', 'remark', 'item_code', 'item_name', 'created_by', 'created_at', ]); // 참조 정보 보강 (work_order 번호 등) $woIds = $transactions->where('reference_type', 'work_order_input') ->pluck('reference_id')->unique()->values(); $woMap = []; if ($woIds->isNotEmpty()) { $woMap = DB::table('work_orders') ->whereIn('id', $woIds) ->pluck('work_order_no', 'id') ->toArray(); } $data = $transactions->map(function ($tx) use ($woMap) { $refLabel = match ($tx->reference_type) { 'work_order_input' => '자재투입', 'work_order_input_cancel' => '자재투입 취소', 'work_order_input_replace' => '자재투입 교체', 'receiving' => '입고', 'adjustment' => '재고조정', 'shipment' => '출하', default => $tx->reference_type ?? '-', }; $refNo = $woMap[$tx->reference_id] ?? null; return [ 'id' => $tx->id, 'type' => $tx->type, 'type_label' => $refLabel, 'qty' => (float) $tx->qty, 'balance_qty' => (float) $tx->balance_qty, 'reference_type' => $tx->reference_type, 'reference_id' => $tx->reference_id, 'reference_no' => $refNo, 'lot_no' => $tx->lot_no, 'reason' => $tx->reason, 'remark' => $tx->remark, 'item_code' => $tx->item_code, 'item_name' => $tx->item_name, 'created_at' => $tx->created_at, ]; }); return ApiResponse::success([ 'item_code' => $stock?->item_code, 'item_name' => $stock?->item_name, 'current_qty' => (float) ($stock?->stock_qty ?? 0), 'available_qty' => (float) ($stock?->available_qty ?? 0), 'transactions' => $data, ]); } }