modelSetService = $modelSetService; $this->calculationEngine = $calculationEngine; $this->pricingService = $pricingService; } /** * 견적 목록 조회 */ public function getEstimates(array $filters = []): LengthAwarePaginator { $query = Estimate::with(['modelSet', 'items']) ->where('tenant_id', $this->tenantId()); // 필터링 if (! empty($filters['status'])) { $query->where('status', $filters['status']); } if (! empty($filters['customer_name'])) { $query->where('customer_name', 'like', '%'.$filters['customer_name'].'%'); } if (! empty($filters['model_set_id'])) { $query->where('model_set_id', $filters['model_set_id']); } if (! empty($filters['date_from'])) { $query->whereDate('created_at', '>=', $filters['date_from']); } if (! empty($filters['date_to'])) { $query->whereDate('created_at', '<=', $filters['date_to']); } if (! empty($filters['search'])) { $searchTerm = $filters['search']; $query->where(function ($q) use ($searchTerm) { $q->where('estimate_name', 'like', '%'.$searchTerm.'%') ->orWhere('estimate_no', 'like', '%'.$searchTerm.'%') ->orWhere('project_name', 'like', '%'.$searchTerm.'%'); }); } return $query->orderBy('created_at', 'desc') ->paginate($filters['per_page'] ?? 20); } /** * 견적 상세 조회 */ public function getEstimateDetail($estimateId): array { $estimate = Estimate::with(['modelSet.fields', 'items']) ->where('tenant_id', $this->tenantId()) ->findOrFail($estimateId); return [ 'estimate' => $estimate, 'model_set_schema' => $this->modelSetService->getModelSetCategoryFields($estimate->model_set_id), 'calculation_summary' => $this->summarizeCalculations($estimate), ]; } /** * 견적 생성 */ public function createEstimate(array $data): array { return DB::transaction(function () use ($data) { // 견적번호 생성 $estimateNo = Estimate::generateEstimateNo($this->tenantId()); // 모델셋 기반 BOM 계산 $bomCalculation = $this->modelSetService->calculateModelSetBom( $data['model_set_id'], $data['parameters'] ); // 견적 생성 $estimate = Estimate::create([ 'tenant_id' => $this->tenantId(), 'model_set_id' => $data['model_set_id'], 'estimate_no' => $estimateNo, 'estimate_name' => $data['estimate_name'], 'customer_name' => $data['customer_name'] ?? null, 'project_name' => $data['project_name'] ?? null, 'parameters' => $data['parameters'], 'calculated_results' => $bomCalculation['calculated_values'] ?? [], 'bom_data' => $bomCalculation, 'total_amount' => 0, // 항목 생성 후 재계산 'notes' => $data['notes'] ?? null, 'valid_until' => now()->addDays(30), // 기본 30일 유효 'created_by' => $this->apiUserId(), ]); // 견적 항목 생성 (BOM 기반) + 가격 계산 if (! empty($bomCalculation['bom_items'])) { $totalAmount = $this->createEstimateItems( $estimate, $bomCalculation['bom_items'], $data['client_id'] ?? null ); // 총액 업데이트 $estimate->update(['total_amount' => $totalAmount]); } return $this->getEstimateDetail($estimate->id); }); } /** * 견적 수정 */ public function updateEstimate($estimateId, array $data): array { return DB::transaction(function () use ($estimateId, $data) { $estimate = Estimate::where('tenant_id', $this->tenantId()) ->findOrFail($estimateId); // 파라미터가 변경되면 재계산 if (isset($data['parameters'])) { $bomCalculation = $this->modelSetService->calculateModelSetBom( $estimate->model_set_id, $data['parameters'] ); $data['calculated_results'] = $bomCalculation['calculated_values'] ?? []; $data['bom_data'] = $bomCalculation; // 기존 견적 항목 삭제 후 재생성 $estimate->items()->delete(); if (! empty($bomCalculation['bom_items'])) { $totalAmount = $this->createEstimateItems( $estimate, $bomCalculation['bom_items'], $data['client_id'] ?? null ); $data['total_amount'] = $totalAmount; } } $estimate->update([ ...$data, 'updated_by' => $this->apiUserId(), ]); return $this->getEstimateDetail($estimate->id); }); } /** * 견적 삭제 */ public function deleteEstimate($estimateId): void { DB::transaction(function () use ($estimateId) { $estimate = Estimate::where('tenant_id', $this->tenantId()) ->findOrFail($estimateId); // 진행 중인 견적은 삭제 불가 if (in_array($estimate->status, ['SENT', 'APPROVED'])) { throw new \Exception(__('error.estimate.cannot_delete_sent_or_approved')); } $estimate->update(['deleted_by' => $this->apiUserId()]); $estimate->delete(); }); } /** * 견적 복제 */ public function cloneEstimate($estimateId, array $data): array { return DB::transaction(function () use ($estimateId, $data) { $originalEstimate = Estimate::with('items') ->where('tenant_id', $this->tenantId()) ->findOrFail($estimateId); // 새 견적번호 생성 $newEstimateNo = Estimate::generateEstimateNo($this->tenantId()); // 견적 복제 $newEstimate = Estimate::create([ 'tenant_id' => $this->tenantId(), 'model_set_id' => $originalEstimate->model_set_id, 'estimate_no' => $newEstimateNo, 'estimate_name' => $data['estimate_name'], 'customer_name' => $data['customer_name'] ?? $originalEstimate->customer_name, 'project_name' => $data['project_name'] ?? $originalEstimate->project_name, 'parameters' => $originalEstimate->parameters, 'calculated_results' => $originalEstimate->calculated_results, 'bom_data' => $originalEstimate->bom_data, 'total_amount' => $originalEstimate->total_amount, 'notes' => $data['notes'] ?? $originalEstimate->notes, 'valid_until' => now()->addDays(30), 'created_by' => $this->apiUserId(), ]); // 견적 항목 복제 foreach ($originalEstimate->items as $item) { EstimateItem::create([ 'tenant_id' => $this->tenantId(), 'estimate_id' => $newEstimate->id, 'sequence' => $item->sequence, 'item_name' => $item->item_name, 'item_description' => $item->item_description, 'parameters' => $item->parameters, 'calculated_values' => $item->calculated_values, 'unit_price' => $item->unit_price, 'quantity' => $item->quantity, 'total_price' => $item->total_price, 'bom_components' => $item->bom_components, 'notes' => $item->notes, 'created_by' => $this->apiUserId(), ]); } return $this->getEstimateDetail($newEstimate->id); }); } /** * 견적 상태 변경 */ public function changeEstimateStatus($estimateId, string $status, ?string $notes = null): array { $estimate = Estimate::where('tenant_id', $this->tenantId()) ->findOrFail($estimateId); $validTransitions = [ 'DRAFT' => ['SENT', 'REJECTED'], 'SENT' => ['APPROVED', 'REJECTED', 'EXPIRED'], 'APPROVED' => ['EXPIRED'], 'REJECTED' => ['DRAFT'], 'EXPIRED' => ['DRAFT'], ]; if (! in_array($status, $validTransitions[$estimate->status] ?? [])) { throw new \Exception(__('error.estimate.invalid_status_transition')); } $estimate->update([ 'status' => $status, 'notes' => $notes ? ($estimate->notes."\n\n".$notes) : $estimate->notes, 'updated_by' => $this->apiUserId(), ]); return $this->getEstimateDetail($estimate->id); } /** * 동적 견적 폼 스키마 조회 */ public function getEstimateFormSchema($modelSetId): array { $parameters = $this->modelSetService->getEstimateParameters($modelSetId); return [ 'model_set' => $parameters['category'], 'form_schema' => [ 'input_fields' => $parameters['input_fields'], 'calculated_fields' => $parameters['calculated_fields'], ], 'calculation_schema' => $parameters['calculation_schema'], ]; } /** * 견적 파라미터 미리보기 계산 */ public function previewCalculation($modelSetId, array $parameters): array { return $this->modelSetService->calculateModelSetBom($modelSetId, $parameters); } /** * 견적 항목 생성 (가격 계산 포함) * * @return float 총 견적 금액 */ protected function createEstimateItems(Estimate $estimate, array $bomItems, ?int $clientId = null): float { $totalAmount = 0; $warnings = []; foreach ($bomItems as $index => $bomItem) { $quantity = $bomItem['quantity'] ?? 1; $unitPrice = 0; // 가격 조회 (item_id와 item_type이 있는 경우) if (isset($bomItem['item_id']) && isset($bomItem['item_type'])) { $priceResult = $this->pricingService->getItemPrice( $bomItem['item_type'], // 'PRODUCT' or 'MATERIAL' $bomItem['item_id'], $clientId, now()->format('Y-m-d') ); $unitPrice = $priceResult['price'] ?? 0; if ($priceResult['warning']) { $warnings[] = $priceResult['warning']; } } $totalPrice = $unitPrice * $quantity; $totalAmount += $totalPrice; EstimateItem::create([ 'tenant_id' => $this->tenantId(), 'estimate_id' => $estimate->id, 'sequence' => $index + 1, 'item_name' => $bomItem['name'] ?? '견적 항목 '.($index + 1), 'item_description' => $bomItem['description'] ?? '', 'parameters' => $bomItem['parameters'] ?? [], 'calculated_values' => $bomItem['calculated_values'] ?? [], 'unit_price' => $unitPrice, 'quantity' => $quantity, 'total_price' => $totalPrice, 'bom_components' => $bomItem['components'] ?? [], 'created_by' => $this->apiUserId(), ]); } // 가격 경고가 있으면 로그 기록 if (! empty($warnings)) { \Log::warning('견적 가격 조회 경고', [ 'estimate_id' => $estimate->id, 'warnings' => $warnings, ]); } return $totalAmount; } /** * 계산 결과 요약 */ protected function summarizeCalculations(Estimate $estimate): array { $summary = [ 'total_items' => $estimate->items->count(), 'total_amount' => $estimate->total_amount, 'key_calculations' => [], ]; // 주요 계산 결과 추출 if (! empty($estimate->calculated_results)) { $results = $estimate->calculated_results; if (isset($results['W1'], $results['H1'])) { $summary['key_calculations']['제작사이즈'] = $results['W1'].' × '.$results['H1'].' mm'; } if (isset($results['weight'])) { $summary['key_calculations']['중량'] = $results['weight'].' kg'; } if (isset($results['area'])) { $summary['key_calculations']['면적'] = $results['area'].' ㎡'; } if (isset($results['bracket_size'])) { $summary['key_calculations']['모터브라켓'] = $results['bracket_size']; } } return $summary; } }