tenantId(); $page = (int) ($params['page'] ?? 1); $size = (int) ($params['size'] ?? 20); $q = trim((string) ($params['q'] ?? '')); $status = $params['status'] ?? null; $processType = $params['process_type'] ?? null; $eagerLoad = ['classificationRules', 'processItems.item:id,code,name', 'steps', 'documentTemplate:id,name,category', 'workLogTemplateRelation:id,name,category', 'parent:id,process_code,process_name', 'children:id,parent_id,process_code,process_name,is_active']; $query = Process::query() ->where('tenant_id', $tenantId) ->with($eagerLoad); // 검색어 if ($q !== '') { $query->where(function ($qq) use ($q) { $qq->where('process_name', 'like', "%{$q}%") ->orWhere('process_code', 'like', "%{$q}%") ->orWhere('description', 'like', "%{$q}%") ->orWhere('department', 'like', "%{$q}%"); }); } // 상태 필터 if ($status === 'active') { $query->where('is_active', true); } elseif ($status === 'inactive') { $query->where('is_active', false); } // 공정구분 필터 if ($processType) { $query->where('process_type', $processType); } $query->orderBy('process_code'); return $query->paginate($size, ['*'], 'page', $page); } /** * 공정 상세 조회 */ public function show(int $id) { $tenantId = $this->tenantId(); $process = Process::where('tenant_id', $tenantId) ->with(['classificationRules', 'processItems.item:id,code,name', 'steps', 'documentTemplate:id,name,category', 'workLogTemplateRelation:id,name,category', 'parent:id,process_code,process_name', 'children:id,parent_id,process_code,process_name,is_active']) ->find($id); if (! $process) { throw new NotFoundHttpException(__('error.not_found')); } return $process; } /** * 공정 생성 */ public function store(array $data) { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($data, $tenantId, $userId) { // 2depth 제한: 부모가 이미 자식이면 거부 if (! empty($data['parent_id'])) { $parent = Process::find($data['parent_id']); if ($parent && $parent->parent_id) { throw \Illuminate\Validation\ValidationException::withMessages([ 'parent_id' => ['2단계까지만 허용됩니다. 선택한 부모 공정이 이미 하위 공정입니다.'], ]); } } // 공정코드 자동 생성 $data['process_code'] = $this->generateProcessCode($tenantId); $data['tenant_id'] = $tenantId; $data['created_by'] = $userId; $data['is_active'] = $data['is_active'] ?? true; // work_steps가 문자열이면 배열로 변환 if (isset($data['work_steps']) && is_string($data['work_steps'])) { $data['work_steps'] = array_map('trim', explode(',', $data['work_steps'])); } $rules = $data['classification_rules'] ?? []; $itemIds = $data['item_ids'] ?? []; unset($data['classification_rules'], $data['item_ids']); $process = Process::create($data); // 분류 규칙 저장 (패턴 규칙) $this->syncClassificationRules($process, $rules); // 개별 품목 연결 $this->syncProcessItems($process, $itemIds); return $process->load(['classificationRules', 'processItems.item:id,code,name', 'steps', 'documentTemplate:id,name,category', 'workLogTemplateRelation:id,name,category']); }); } /** * 공정 수정 */ public function update(int $id, array $data) { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $process = Process::where('tenant_id', $tenantId)->find($id); if (! $process) { throw new NotFoundHttpException(__('error.not_found')); } return DB::transaction(function () use ($process, $data, $userId) { // parent_id 변경 시 2depth + 순환 참조 검증 if (array_key_exists('parent_id', $data) && $data['parent_id']) { $parent = Process::find($data['parent_id']); if ($parent && $parent->parent_id) { throw \Illuminate\Validation\ValidationException::withMessages([ 'parent_id' => ['2단계까지만 허용됩니다.'], ]); } // 자기 자식을 부모로 설정하는 것 방지 if ($process->children()->where('id', $data['parent_id'])->exists()) { throw \Illuminate\Validation\ValidationException::withMessages([ 'parent_id' => ['하위 공정을 부모로 설정할 수 없습니다.'], ]); } } $data['updated_by'] = $userId; // work_steps가 문자열이면 배열로 변환 if (isset($data['work_steps']) && is_string($data['work_steps'])) { $data['work_steps'] = array_map('trim', explode(',', $data['work_steps'])); } $rules = $data['classification_rules'] ?? null; $itemIds = $data['item_ids'] ?? null; unset($data['classification_rules'], $data['item_ids']); $process->update($data); // 분류 규칙 동기화 (전달된 경우만) if ($rules !== null) { $this->syncClassificationRules($process, $rules); } // 개별 품목 동기화 (전달된 경우만) if ($itemIds !== null) { $this->syncProcessItems($process, $itemIds); } return $process->fresh(['classificationRules', 'processItems.item:id,code,name', 'steps', 'documentTemplate:id,name,category', 'workLogTemplateRelation:id,name,category']); }); } /** * 공정 삭제 */ public function destroy(int $id) { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $process = Process::where('tenant_id', $tenantId)->find($id); if (! $process) { throw new NotFoundHttpException(__('error.not_found')); } $process->update(['deleted_by' => $userId]); $process->delete(); return true; } /** * 공정 일괄 삭제 */ public function destroyMany(array $ids) { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $count = Process::where('tenant_id', $tenantId) ->whereIn('id', $ids) ->update(['deleted_by' => $userId, 'deleted_at' => now()]); return $count; } /** * 공정 상태 토글 */ public function toggleActive(int $id) { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $process = Process::where('tenant_id', $tenantId)->find($id); if (! $process) { throw new NotFoundHttpException(__('error.not_found')); } $process->update([ 'is_active' => ! $process->is_active, 'updated_by' => $userId, ]); return $process->fresh(['classificationRules', 'processItems.item:id,code,name', 'steps', 'documentTemplate:id,name,category', 'workLogTemplateRelation:id,name,category']); } /** * 공정코드 자동 생성 (P-001, P-002, ...) */ private function generateProcessCode(int $tenantId): string { $lastProcess = Process::where('tenant_id', $tenantId) ->withTrashed() ->orderByRaw('CAST(SUBSTRING(process_code, 3) AS UNSIGNED) DESC') ->first(); if ($lastProcess && preg_match('/^P-(\d+)$/', $lastProcess->process_code, $matches)) { $nextNum = (int) $matches[1] + 1; } else { $nextNum = 1; } return sprintf('P-%03d', $nextNum); } /** * 분류 규칙 동기화 (패턴 규칙용) */ private function syncClassificationRules(Process $process, array $rules): void { // 기존 규칙 삭제 $process->classificationRules()->delete(); // 새 규칙 생성 (패턴 규칙만) foreach ($rules as $index => $rule) { ProcessClassificationRule::create([ 'process_id' => $process->id, 'registration_type' => 'pattern', 'rule_type' => $rule['rule_type'], 'matching_type' => $rule['matching_type'], 'condition_value' => $rule['condition_value'], 'priority' => $rule['priority'] ?? $index, 'description' => $rule['description'] ?? null, 'is_active' => $rule['is_active'] ?? true, ]); } } /** * 개별 품목 연결 동기화 * * @param array $itemIds 품목 ID 배열 */ private function syncProcessItems(Process $process, array $itemIds): void { // 기존 연결 삭제 $process->processItems()->delete(); // 새 연결 생성 foreach ($itemIds as $index => $itemId) { ProcessItem::create([ 'process_id' => $process->id, 'item_id' => $itemId, 'priority' => $index, 'is_active' => true, ]); } } /** * 공정 복제 */ public function duplicate(int $id) { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $source = Process::where('tenant_id', $tenantId) ->with(['classificationRules', 'processItems', 'steps']) ->find($id); if (! $source) { throw new NotFoundHttpException(__('error.not_found')); } return DB::transaction(function () use ($source, $tenantId, $userId) { $newCode = $this->generateProcessCode($tenantId); $newProcess = Process::create([ 'tenant_id' => $tenantId, 'parent_id' => $source->parent_id, 'process_code' => $newCode, 'process_name' => $source->process_name.' (복사)', 'description' => $source->description, 'process_type' => $source->process_type, 'department' => $source->department, 'manager' => $source->manager, 'process_category' => $source->process_category, 'use_production_date' => $source->use_production_date, 'work_log_template' => $source->work_log_template, 'document_template_id' => $source->document_template_id, 'work_log_template_id' => $source->work_log_template_id, 'options' => $source->options, 'required_workers' => $source->required_workers, 'equipment_info' => $source->equipment_info, 'work_steps' => $source->work_steps, 'note' => $source->note, 'is_active' => $source->is_active, 'created_by' => $userId, ]); // 분류 규칙 복제 foreach ($source->classificationRules as $rule) { ProcessClassificationRule::create([ 'process_id' => $newProcess->id, 'registration_type' => $rule->registration_type, 'rule_type' => $rule->rule_type, 'matching_type' => $rule->matching_type, 'condition_value' => $rule->condition_value, 'priority' => $rule->priority, 'description' => $rule->description, 'is_active' => $rule->is_active, ]); } // 품목 연결 복제 foreach ($source->processItems as $item) { ProcessItem::create([ 'process_id' => $newProcess->id, 'item_id' => $item->item_id, 'priority' => $item->priority, 'is_active' => $item->is_active, ]); } // 공정 단계 복제 foreach ($source->steps as $step) { $newProcess->steps()->create([ 'step_code' => $step->step_code, 'step_name' => $step->step_name, 'is_required' => $step->is_required, 'needs_approval' => $step->needs_approval, 'needs_inspection' => $step->needs_inspection, 'is_active' => $step->is_active, 'sort_order' => $step->sort_order, 'connection_type' => $step->connection_type, 'connection_target' => $step->connection_target, 'completion_type' => $step->completion_type, 'options' => $step->options, ]); } return $newProcess->load(['classificationRules', 'processItems.item:id,code,name', 'steps', 'documentTemplate:id,name,category', 'workLogTemplateRelation:id,name,category']); }); } /** * 드롭다운용 공정 옵션 목록 */ public function options() { $tenantId = $this->tenantId(); return Process::where('tenant_id', $tenantId) ->where('is_active', true) ->orderBy('process_code') ->select('id', 'process_code', 'process_name', 'process_type', 'department') ->get(); } /** * 통계 */ public function getStats() { $tenantId = $this->tenantId(); $total = Process::where('tenant_id', $tenantId)->count(); $active = Process::where('tenant_id', $tenantId)->where('is_active', true)->count(); $inactive = Process::where('tenant_id', $tenantId)->where('is_active', false)->count(); $byType = Process::where('tenant_id', $tenantId) ->where('is_active', true) ->groupBy('process_type') ->selectRaw('process_type, count(*) as count') ->pluck('count', 'process_type'); return [ 'total' => $total, 'active' => $active, 'inactive' => $inactive, 'by_type' => $byType, ]; } }