diff --git a/app/Http/Controllers/Api/Admin/ItemManagementApiController.php b/app/Http/Controllers/Api/Admin/ItemManagementApiController.php index b83d0657..d88e2199 100644 --- a/app/Http/Controllers/Api/Admin/ItemManagementApiController.php +++ b/app/Http/Controllers/Api/Admin/ItemManagementApiController.php @@ -54,6 +54,26 @@ public function detail(int $id): View ]); } + /** + * 품목 삭제 (Soft Delete, 사용 중 체크) + */ + public function destroy(int $id): JsonResponse + { + $result = $this->service->deleteItem($id); + + return response()->json($result, $result['success'] ? 200 : 422); + } + + /** + * 품목 이력 조회 (audit_logs 기반) + */ + public function history(int $id): JsonResponse + { + $history = $this->service->getItemHistory($id); + + return response()->json($history); + } + /** * 수식 기반 BOM 산출 (API 서버의 FormulaEvaluatorService HTTP 호출) */ diff --git a/app/Services/ItemManagementService.php b/app/Services/ItemManagementService.php index 1ed13b75..38bd2fbe 100644 --- a/app/Services/ItemManagementService.php +++ b/app/Services/ItemManagementService.php @@ -2,8 +2,10 @@ namespace App\Services; +use App\Models\Audit\AuditLog; use App\Models\Items\Item; use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Support\Facades\DB; class ItemManagementService { @@ -86,6 +88,151 @@ public function getItemDetail(int $itemId): array ]; } + /** + * 품목 사용 현황 조회 (삭제 전 참조 체크) + */ + public function checkItemUsage(int $itemId): array + { + $tenantId = session('selected_tenant_id'); + $usage = []; + + // 다른 품목의 BOM에 포함되어 있는지 (JSON 검색) + $bomParents = Item::withoutGlobalScopes() + ->where('tenant_id', $tenantId) + ->whereRaw("JSON_CONTAINS(bom, JSON_OBJECT('child_item_id', ?))", [$itemId]) + ->count(); + if ($bomParents > 0) { + $usage[] = "다른 품목의 BOM 구성품 ({$bomParents}건)"; + } + + // 주문 항목 + $orderItems = DB::table('order_items')->where('item_id', $itemId)->count(); + if ($orderItems > 0) { + $usage[] = "수주 항목 ({$orderItems}건)"; + } + + // 견적 + $quotes = DB::table('quotes')->where('item_id', $itemId)->count(); + if ($quotes > 0) { + $usage[] = "견적 ({$quotes}건)"; + } + + // 자재 입고 + $receipts = DB::table('material_receipts')->where('item_id', $itemId)->count(); + if ($receipts > 0) { + $usage[] = "자재 입고 ({$receipts}건)"; + } + + // LOT + $lots = DB::table('lots')->where('item_id', $itemId)->count(); + if ($lots > 0) { + $usage[] = "LOT ({$lots}건)"; + } + + // 작업지시 + $workOrders = DB::table('work_order_items')->where('item_id', $itemId)->count(); + if ($workOrders > 0) { + $usage[] = "작업지시 ({$workOrders}건)"; + } + + return $usage; + } + + /** + * 품목 삭제 (Soft Delete) + */ + public function deleteItem(int $itemId): array + { + $tenantId = session('selected_tenant_id'); + + $item = Item::withoutGlobalScopes() + ->where('tenant_id', $tenantId) + ->findOrFail($itemId); + + // 사용 현황 체크 + $usage = $this->checkItemUsage($itemId); + if (! empty($usage)) { + return [ + 'success' => false, + 'error' => '사용 중인 품목은 삭제할 수 없습니다.', + 'usage' => $usage, + ]; + } + + $item->deleted_by = auth()->id(); + $item->save(); + $item->delete(); + + return ['success' => true, 'message' => "품목 '{$item->name}'이(가) 삭제되었습니다."]; + } + + /** + * 품목 이력 조회 (audit_logs + 생성/수정 정보) + */ + public function getItemHistory(int $itemId): array + { + $tenantId = session('selected_tenant_id'); + + $item = Item::withoutGlobalScopes() + ->where('tenant_id', $tenantId) + ->findOrFail($itemId); + + // audit_logs에서 해당 품목 이력 + $auditLogs = AuditLog::where('tenant_id', $tenantId) + ->where('target_type', 'item') + ->where('target_id', $itemId) + ->orderByDesc('created_at') + ->limit(50) + ->get(); + + // 사용자 ID → 이름 매핑 + $actorIds = $auditLogs->pluck('actor_id')->filter()->unique()->values(); + $actors = []; + if ($actorIds->isNotEmpty()) { + $actors = DB::table('users') + ->whereIn('id', $actorIds) + ->pluck('name', 'id') + ->toArray(); + } + + $logs = $auditLogs->map(function ($log) use ($actors) { + return [ + 'id' => $log->id, + 'action' => $log->action, + 'action_label' => $this->getActionLabel($log->action), + 'actor' => $actors[$log->actor_id] ?? '시스템', + 'created_at' => $log->created_at->format('Y-m-d H:i:s'), + 'before' => $log->before, + 'after' => $log->after, + ]; + }); + + return [ + 'item' => [ + 'id' => $item->id, + 'code' => $item->code, + 'name' => $item->name, + 'created_at' => $item->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $item->updated_at?->format('Y-m-d H:i:s'), + ], + 'logs' => $logs, + ]; + } + + private function getActionLabel(string $action): string + { + return match ($action) { + 'created' => '생성', + 'updated' => '수정', + 'deleted' => '삭제', + 'restored' => '복원', + 'bom_updated' => 'BOM 변경', + 'stock_increase' => '재고 증가', + 'stock_decrease' => '재고 차감', + default => $action, + }; + } + // ── Private ── private function buildBomNode(Item $item, int $depth, int $maxDepth, array $visited): array diff --git a/resources/views/item-management/index.blade.php b/resources/views/item-management/index.blade.php index e9c792c6..85cfec36 100644 --- a/resources/views/item-management/index.blade.php +++ b/resources/views/item-management/index.blade.php @@ -144,6 +144,20 @@ class="px-4 py-1.5 text-sm font-medium text-white bg-blue-600 rounded hover:bg-b + + {{-- 이력 조회 모달 --}} +
품목을 선택하세요
'; + document.getElementById('bom-tree-container').innerHTML = ''; + loadItemList(); + } else { + let msg = data.error || '삭제 실패'; + if (data.usage && data.usage.length > 0) { + msg += '\n\n참조 현황:\n- ' + data.usage.join('\n- '); + } + alert(msg); + } + }) + .catch(err => { + console.error('deleteItem error:', err); + alert('삭제 요청 중 오류가 발생했습니다.'); + }); +}; + +// ── 품목 이력 조회 ── +window.showItemHistory = function(itemId) { + const modal = document.getElementById('history-modal'); + const body = document.getElementById('history-modal-body'); + body.innerHTML = '기록된 이력이 없습니다.
'; + } else { + html += '이력 조회에 실패했습니다.
'; + }); +}; + +window.closeHistoryModal = function() { + document.getElementById('history-modal').classList.add('hidden'); +}; @endpush diff --git a/resources/views/item-management/partials/item-detail.blade.php b/resources/views/item-management/partials/item-detail.blade.php index b7221612..e1d86551 100644 --- a/resources/views/item-management/partials/item-detail.blade.php +++ b/resources/views/item-management/partials/item-detail.blade.php @@ -5,6 +5,26 @@ data-item-type="{{ $item->item_type }}" style="display:none;"> +{{-- 액션 버튼 --}} +