validateInputs($inputs, $category); // DB에서 수식 조회 $formulasByCategory = $this->getFormulasByCategory($productId); if ($formulasByCategory->isEmpty()) { return [ 'inputs' => $validatedInputs, 'outputs' => [], 'items' => [], 'costs' => [ 'material_cost' => 0, 'labor_cost' => 0, 'install_cost' => 0, 'subtotal' => 0, ], 'errors' => [__('error.no_formulas_configured')], ]; } // 수식 실행 $result = $this->formulaEvaluator->executeAll($formulasByCategory, $validatedInputs); // 비용 계산 $costs = $this->calculateCosts($result['items']); return [ 'inputs' => $validatedInputs, 'outputs' => $result['variables'], 'items' => $result['items'], 'costs' => $costs, 'errors' => $result['errors'], ]; } /** * 견적 미리보기 (저장 없이 계산만) */ public function preview(array $inputs, ?string $productCategory = null, ?int $productId = null): array { return $this->calculate($inputs, $productCategory, $productId); } /** * 견적 품목 재계산 (기존 견적 기준) */ public function recalculate(Quote $quote): array { $inputs = $quote->calculation_inputs ?? []; $category = $quote->product_category; $productId = $quote->product_id; return $this->calculate($inputs, $category, $productId); } /** * 카테고리별 수식 조회 * * @param int|null $productId 제품 ID (공통 수식 + 제품별 수식 조회) * @return Collection 카테고리 코드 => 수식 목록 */ private function getFormulasByCategory(?int $productId = null): Collection { $tenantId = $this->tenantId(); if (! $tenantId) { return collect(); } // 카테고리 조회 (정렬순) $categories = QuoteFormulaCategory::query() ->where('tenant_id', $tenantId) ->active() ->ordered() ->with([ 'formulas' => function ($query) use ($productId) { $query->active() ->forProduct($productId) ->ordered() ->with(['ranges', 'mappings', 'items']); }, ]) ->get(); // 카테고리 코드 => 수식 목록으로 변환 return $categories->mapWithKeys(function ($category) { return [$category->code => $category->formulas]; })->filter(fn ($formulas) => $formulas->isNotEmpty()); } /** * 입력값 검증 및 기본값 설정 */ private function validateInputs(array $inputs, string $category): array { // 공통 입력값 $validated = [ 'W0' => (float) ($inputs['W0'] ?? $inputs['open_size_width'] ?? 0), 'H0' => (float) ($inputs['H0'] ?? $inputs['open_size_height'] ?? 0), 'QTY' => (int) ($inputs['QTY'] ?? $inputs['quantity'] ?? 1), ]; // 카테고리별 추가 입력값 if ($category === Quote::CATEGORY_SCREEN) { $validated = array_merge($validated, [ 'INSTALL_TYPE' => $inputs['INSTALL_TYPE'] ?? 'wall', // wall, ceiling, floor 'MOTOR_TYPE' => $inputs['MOTOR_TYPE'] ?? 'standard', // standard, heavy 'CONTROL_TYPE' => $inputs['CONTROL_TYPE'] ?? 'switch', // switch, remote, smart 'CHAIN_SIDE' => $inputs['CHAIN_SIDE'] ?? 'left', // left, right ]); } elseif ($category === Quote::CATEGORY_STEEL) { $validated = array_merge($validated, [ 'MATERIAL' => $inputs['MATERIAL'] ?? 'ss304', // ss304, ss316, galvanized 'THICKNESS' => (float) ($inputs['THICKNESS'] ?? 1.5), 'FINISH' => $inputs['FINISH'] ?? 'hairline', // hairline, mirror, matte 'WELDING' => $inputs['WELDING'] ?? 'tig', // tig, mig, spot ]); } // 추가 사용자 입력값 병합 (DB 수식에서 사용할 수 있도록) foreach ($inputs as $key => $value) { if (! isset($validated[$key])) { $validated[$key] = $value; } } return $validated; } /** * 비용 계산 */ private function calculateCosts(array $items): array { $materialCost = 0; $laborCost = 0; $installCost = 0; foreach ($items as $item) { $category = $item['formula_category'] ?? 'material'; $price = (float) ($item['total_price'] ?? 0); match ($category) { 'material' => $materialCost += $price, 'labor' => $laborCost += $price, 'install' => $installCost += $price, default => $materialCost += $price, }; } $subtotal = $materialCost + $laborCost + $installCost; return [ 'material_cost' => round($materialCost, 2), 'labor_cost' => round($laborCost, 2), 'install_cost' => round($installCost, 2), 'subtotal' => round($subtotal, 2), ]; } /** * 입력 스키마 반환 (프론트엔드용) * DB에서 input 타입 수식을 조회하여 동적으로 생성 */ public function getInputSchema(?string $productCategory = null, ?int $productId = null): array { $tenantId = $this->tenantId(); if (! $tenantId) { return $this->getDefaultInputSchema($productCategory); } // DB에서 input 타입 수식 조회 $inputFormulas = QuoteFormula::query() ->where('tenant_id', $tenantId) ->active() ->forProduct($productId) ->ofType(QuoteFormula::TYPE_INPUT) ->ordered() ->get(); if ($inputFormulas->isEmpty()) { return $this->getDefaultInputSchema($productCategory); } $schema = []; foreach ($inputFormulas as $formula) { $schema[$formula->variable] = [ 'label' => $formula->name, 'type' => 'number', // 기본값, 추후 formula.description에서 메타 정보 파싱 가능 'required' => true, 'description' => $formula->description, ]; } return $schema; } /** * 기본 입력 스키마 (DB에 수식이 없을 때 사용) */ private function getDefaultInputSchema(?string $productCategory = null): array { $category = $productCategory ?? Quote::CATEGORY_SCREEN; $commonSchema = [ 'W0' => [ 'label' => '개구부 폭', 'type' => 'number', 'unit' => 'mm', 'required' => true, 'min' => 100, 'max' => 10000, ], 'H0' => [ 'label' => '개구부 높이', 'type' => 'number', 'unit' => 'mm', 'required' => true, 'min' => 100, 'max' => 10000, ], 'QTY' => [ 'label' => '수량', 'type' => 'integer', 'required' => true, 'min' => 1, 'default' => 1, ], ]; if ($category === Quote::CATEGORY_SCREEN) { return array_merge($commonSchema, [ 'INSTALL_TYPE' => [ 'label' => '설치 유형', 'type' => 'select', 'options' => [ ['value' => 'wall', 'label' => '벽면'], ['value' => 'ceiling', 'label' => '천장'], ['value' => 'floor', 'label' => '바닥'], ], 'default' => 'wall', ], 'MOTOR_TYPE' => [ 'label' => '모터 유형', 'type' => 'select', 'options' => [ ['value' => 'standard', 'label' => '일반형'], ['value' => 'heavy', 'label' => '고하중형'], ], 'default' => 'standard', ], 'CONTROL_TYPE' => [ 'label' => '제어 방식', 'type' => 'select', 'options' => [ ['value' => 'switch', 'label' => '스위치'], ['value' => 'remote', 'label' => '리모컨'], ['value' => 'smart', 'label' => '스마트'], ], 'default' => 'switch', ], 'CHAIN_SIDE' => [ 'label' => '체인 위치', 'type' => 'select', 'options' => [ ['value' => 'left', 'label' => '좌측'], ['value' => 'right', 'label' => '우측'], ], 'default' => 'left', ], ]); } if ($category === Quote::CATEGORY_STEEL) { return array_merge($commonSchema, [ 'MATERIAL' => [ 'label' => '재질', 'type' => 'select', 'options' => [ ['value' => 'ss304', 'label' => 'SUS304'], ['value' => 'ss316', 'label' => 'SUS316'], ['value' => 'galvanized', 'label' => '아연도금'], ], 'default' => 'ss304', ], 'THICKNESS' => [ 'label' => '두께', 'type' => 'number', 'unit' => 'mm', 'min' => 0.5, 'max' => 10, 'step' => 0.1, 'default' => 1.5, ], 'FINISH' => [ 'label' => '표면처리', 'type' => 'select', 'options' => [ ['value' => 'hairline', 'label' => '헤어라인'], ['value' => 'mirror', 'label' => '미러'], ['value' => 'matte', 'label' => '무광'], ], 'default' => 'hairline', ], 'WELDING' => [ 'label' => '용접 방식', 'type' => 'select', 'options' => [ ['value' => 'tig', 'label' => 'TIG'], ['value' => 'mig', 'label' => 'MIG'], ['value' => 'spot', 'label' => '스팟'], ], 'default' => 'tig', ], ]); } return $commonSchema; } }