refactor: 견적 산출 서비스 DB 기반으로 재작성
- Quote 수식 모델 추가 (mng 패턴 적용) - QuoteFormula: 수식 정의 (input/calculation/range/mapping) - QuoteFormulaCategory: 카테고리 정의 - QuoteFormulaItem: 품목 출력 정의 - QuoteFormulaRange: 범위별 값 정의 - QuoteFormulaMapping: 매핑 값 정의 - FormulaEvaluatorService 확장 - executeAll(): 카테고리별 수식 실행 - evaluateRangeFormula/evaluateMappingFormula: QuoteFormula 기반 평가 - getItemPrice(): prices 테이블 연동 - QuoteCalculationService DB 기반으로 재작성 - 하드코딩된 품목 코드/로직 제거 - quote_formulas 테이블 기반 동적 계산 - getInputSchema(): DB 기반 입력 스키마 생성 - Price 모델 수정 - items 테이블 연동 (products/materials 대체) - ITEM_TYPE 상수 업데이트 (FG/PT/RM/SM/CS)
This commit is contained in:
@@ -29,10 +29,16 @@ class Price extends Model
|
||||
|
||||
public const STATUS_FINALIZED = 'finalized';
|
||||
|
||||
// 품목 유형
|
||||
public const ITEM_TYPE_PRODUCT = 'PRODUCT';
|
||||
// 품목 유형 (items.item_type)
|
||||
public const ITEM_TYPE_FG = 'FG'; // Finished Goods (완제품)
|
||||
|
||||
public const ITEM_TYPE_MATERIAL = 'MATERIAL';
|
||||
public const ITEM_TYPE_PT = 'PT'; // Part (부품)
|
||||
|
||||
public const ITEM_TYPE_RM = 'RM'; // Raw Material (원자재)
|
||||
|
||||
public const ITEM_TYPE_SM = 'SM'; // Semi-finished (반제품)
|
||||
|
||||
public const ITEM_TYPE_CS = 'CS'; // Consumables/Supplies (소모품)
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
@@ -289,7 +295,6 @@ public static function getCurrentPrice(
|
||||
|
||||
/**
|
||||
* 품목 코드로 현재 유효 판매단가 조회
|
||||
* (quote_formula_items.item_code와 연동용)
|
||||
*
|
||||
* @param int $tenantId 테넌트 ID
|
||||
* @param string $itemCode 품목 코드
|
||||
@@ -297,32 +302,19 @@ public static function getCurrentPrice(
|
||||
*/
|
||||
public static function getSalesPriceByItemCode(int $tenantId, string $itemCode): float
|
||||
{
|
||||
// products 테이블에서 품목 코드로 검색
|
||||
$product = \Illuminate\Support\Facades\DB::table('products')
|
||||
// items 테이블에서 품목 코드로 검색
|
||||
$item = \Illuminate\Support\Facades\DB::table('items')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $itemCode)
|
||||
->whereNull('deleted_at')
|
||||
->first();
|
||||
|
||||
if ($product) {
|
||||
$price = static::getCurrentPrice($tenantId, self::ITEM_TYPE_PRODUCT, $product->id);
|
||||
|
||||
return (float) ($price?->sales_price ?? 0);
|
||||
if (! $item) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// materials 테이블에서도 검색
|
||||
$material = \Illuminate\Support\Facades\DB::table('materials')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $itemCode)
|
||||
->whereNull('deleted_at')
|
||||
->first();
|
||||
$price = static::getCurrentPrice($tenantId, $item->item_type, $item->id);
|
||||
|
||||
if ($material) {
|
||||
$price = static::getCurrentPrice($tenantId, self::ITEM_TYPE_MATERIAL, $material->id);
|
||||
|
||||
return (float) ($price?->sales_price ?? 0);
|
||||
}
|
||||
|
||||
return 0;
|
||||
return (float) ($price?->sales_price ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
219
app/Models/Quote/QuoteFormula.php
Normal file
219
app/Models/Quote/QuoteFormula.php
Normal file
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Quote;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 견적 수식 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property int $category_id
|
||||
* @property int|null $product_id
|
||||
* @property string $name
|
||||
* @property string $variable
|
||||
* @property string $type
|
||||
* @property string|null $formula
|
||||
* @property string $output_type
|
||||
* @property string|null $description
|
||||
* @property int $sort_order
|
||||
* @property bool $is_active
|
||||
*/
|
||||
class QuoteFormula extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'quote_formulas';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'category_id',
|
||||
'product_id',
|
||||
'name',
|
||||
'variable',
|
||||
'type',
|
||||
'formula',
|
||||
'output_type',
|
||||
'description',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'type' => 'calculation',
|
||||
'output_type' => 'variable',
|
||||
'sort_order' => 0,
|
||||
'is_active' => true,
|
||||
];
|
||||
|
||||
// 수식 유형 상수
|
||||
public const TYPE_INPUT = 'input';
|
||||
|
||||
public const TYPE_CALCULATION = 'calculation';
|
||||
|
||||
public const TYPE_RANGE = 'range';
|
||||
|
||||
public const TYPE_MAPPING = 'mapping';
|
||||
|
||||
// 출력 유형 상수
|
||||
public const OUTPUT_VARIABLE = 'variable';
|
||||
|
||||
public const OUTPUT_ITEM = 'item';
|
||||
|
||||
// =========================================================================
|
||||
// Relationships
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 카테고리 관계
|
||||
*/
|
||||
public function category(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(QuoteFormulaCategory::class, 'category_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 범위 규칙
|
||||
*/
|
||||
public function ranges(): HasMany
|
||||
{
|
||||
return $this->hasMany(QuoteFormulaRange::class, 'formula_id')
|
||||
->orderBy('sort_order');
|
||||
}
|
||||
|
||||
/**
|
||||
* 매핑 규칙
|
||||
*/
|
||||
public function mappings(): HasMany
|
||||
{
|
||||
return $this->hasMany(QuoteFormulaMapping::class, 'formula_id')
|
||||
->orderBy('sort_order');
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 출력
|
||||
*/
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(QuoteFormulaItem::class, 'formula_id')
|
||||
->orderBy('sort_order');
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성자
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정자
|
||||
*/
|
||||
public function updater(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Scopes
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 수식이 공통 수식인지 확인
|
||||
*/
|
||||
public function isCommon(): bool
|
||||
{
|
||||
return is_null($this->product_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: 공통 수식
|
||||
*/
|
||||
public function scopeCommon(Builder $query): Builder
|
||||
{
|
||||
return $query->whereNull('product_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: 특정 제품 수식 (공통 + 제품 전용)
|
||||
*/
|
||||
public function scopeForProduct(Builder $query, ?int $productId): Builder
|
||||
{
|
||||
return $query->where(function ($q) use ($productId) {
|
||||
$q->whereNull('product_id');
|
||||
if ($productId) {
|
||||
$q->orWhere('product_id', $productId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: 활성화된 수식
|
||||
*/
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: 정렬 순서
|
||||
*/
|
||||
public function scopeOrdered(Builder $query): Builder
|
||||
{
|
||||
return $query->orderBy('sort_order');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: 유형별 필터
|
||||
*/
|
||||
public function scopeOfType(Builder $query, string $type): Builder
|
||||
{
|
||||
return $query->where('type', $type);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 유형 레이블 조회
|
||||
*/
|
||||
public function getTypeLabelAttribute(): string
|
||||
{
|
||||
return match ($this->type) {
|
||||
self::TYPE_INPUT => '입력값',
|
||||
self::TYPE_CALCULATION => '계산식',
|
||||
self::TYPE_RANGE => '범위별',
|
||||
self::TYPE_MAPPING => '매핑',
|
||||
default => $this->type,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 출력 유형 레이블 조회
|
||||
*/
|
||||
public function getOutputTypeLabelAttribute(): string
|
||||
{
|
||||
return match ($this->output_type) {
|
||||
self::OUTPUT_VARIABLE => '변수',
|
||||
self::OUTPUT_ITEM => '품목',
|
||||
default => $this->output_type,
|
||||
};
|
||||
}
|
||||
}
|
||||
110
app/Models/Quote/QuoteFormulaCategory.php
Normal file
110
app/Models/Quote/QuoteFormulaCategory.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Quote;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 견적 수식 카테고리 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property string $code
|
||||
* @property string $name
|
||||
* @property string|null $description
|
||||
* @property int $sort_order
|
||||
* @property bool $is_active
|
||||
* @property int|null $created_by
|
||||
* @property int|null $updated_by
|
||||
*/
|
||||
class QuoteFormulaCategory extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'quote_formula_categories';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'code',
|
||||
'name',
|
||||
'description',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'sort_order' => 0,
|
||||
'is_active' => true,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// Relationships
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 카테고리에 속한 수식들
|
||||
*/
|
||||
public function formulas(): HasMany
|
||||
{
|
||||
return $this->hasMany(QuoteFormula::class, 'category_id')
|
||||
->orderBy('sort_order');
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성화된 수식만
|
||||
*/
|
||||
public function activeFormulas(): HasMany
|
||||
{
|
||||
return $this->formulas()->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성자
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정자
|
||||
*/
|
||||
public function updater(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Scopes
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Scope: 활성화된 카테고리
|
||||
*/
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: 정렬 순서
|
||||
*/
|
||||
public function scopeOrdered(Builder $query): Builder
|
||||
{
|
||||
return $query->orderBy('sort_order');
|
||||
}
|
||||
}
|
||||
70
app/Models/Quote/QuoteFormulaItem.php
Normal file
70
app/Models/Quote/QuoteFormulaItem.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Quote;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 수식 품목 출력 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $formula_id
|
||||
* @property string $item_code
|
||||
* @property string $item_name
|
||||
* @property string|null $specification
|
||||
* @property string $unit
|
||||
* @property string $quantity_formula
|
||||
* @property string|null $unit_price_formula
|
||||
* @property int $sort_order
|
||||
*/
|
||||
class QuoteFormulaItem extends Model
|
||||
{
|
||||
protected $table = 'quote_formula_items';
|
||||
|
||||
protected $fillable = [
|
||||
'formula_id',
|
||||
'item_code',
|
||||
'item_name',
|
||||
'specification',
|
||||
'unit',
|
||||
'quantity_formula',
|
||||
'unit_price_formula',
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'sort_order' => 0,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// Relationships
|
||||
// =========================================================================
|
||||
|
||||
public function formula(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(QuoteFormula::class, 'formula_id');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 품목 표시 문자열
|
||||
*/
|
||||
public function getDisplayNameAttribute(): string
|
||||
{
|
||||
$name = "[{$this->item_code}] {$this->item_name}";
|
||||
|
||||
if ($this->specification) {
|
||||
$name .= " ({$this->specification})";
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
65
app/Models/Quote/QuoteFormulaMapping.php
Normal file
65
app/Models/Quote/QuoteFormulaMapping.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Quote;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 수식 매핑 값 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $formula_id
|
||||
* @property string $source_variable
|
||||
* @property string $source_value
|
||||
* @property string $result_value
|
||||
* @property string $result_type
|
||||
* @property int $sort_order
|
||||
*/
|
||||
class QuoteFormulaMapping extends Model
|
||||
{
|
||||
protected $table = 'quote_formula_mappings';
|
||||
|
||||
protected $fillable = [
|
||||
'formula_id',
|
||||
'source_variable',
|
||||
'source_value',
|
||||
'result_value',
|
||||
'result_type',
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'result_type' => 'fixed',
|
||||
'sort_order' => 0,
|
||||
];
|
||||
|
||||
public const RESULT_FIXED = 'fixed';
|
||||
|
||||
public const RESULT_FORMULA = 'formula';
|
||||
|
||||
// =========================================================================
|
||||
// Relationships
|
||||
// =========================================================================
|
||||
|
||||
public function formula(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(QuoteFormula::class, 'formula_id');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 매핑 조건 표시 문자열
|
||||
*/
|
||||
public function getConditionLabelAttribute(): string
|
||||
{
|
||||
return "{$this->source_variable} = '{$this->source_value}'";
|
||||
}
|
||||
}
|
||||
107
app/Models/Quote/QuoteFormulaRange.php
Normal file
107
app/Models/Quote/QuoteFormulaRange.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Quote;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 수식 범위별 값 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $formula_id
|
||||
* @property float|null $min_value
|
||||
* @property float|null $max_value
|
||||
* @property string $condition_variable
|
||||
* @property string $result_value
|
||||
* @property string $result_type
|
||||
* @property int $sort_order
|
||||
*/
|
||||
class QuoteFormulaRange extends Model
|
||||
{
|
||||
protected $table = 'quote_formula_ranges';
|
||||
|
||||
protected $fillable = [
|
||||
'formula_id',
|
||||
'min_value',
|
||||
'max_value',
|
||||
'condition_variable',
|
||||
'result_value',
|
||||
'result_type',
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'min_value' => 'decimal:4',
|
||||
'max_value' => 'decimal:4',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'result_type' => 'fixed',
|
||||
'sort_order' => 0,
|
||||
];
|
||||
|
||||
public const RESULT_FIXED = 'fixed';
|
||||
|
||||
public const RESULT_FORMULA = 'formula';
|
||||
|
||||
// =========================================================================
|
||||
// Relationships
|
||||
// =========================================================================
|
||||
|
||||
public function formula(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(QuoteFormula::class, 'formula_id');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 값이 범위 내에 있는지 확인
|
||||
*/
|
||||
public function isInRange($value): bool
|
||||
{
|
||||
$min = $this->min_value;
|
||||
$max = $this->max_value;
|
||||
|
||||
if (is_null($min) && is_null($max)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_null($min)) {
|
||||
return $value <= $max;
|
||||
}
|
||||
|
||||
if (is_null($max)) {
|
||||
return $value >= $min;
|
||||
}
|
||||
|
||||
return $value >= $min && $value <= $max;
|
||||
}
|
||||
|
||||
/**
|
||||
* 범위 표시 문자열
|
||||
*/
|
||||
public function getRangeLabelAttribute(): string
|
||||
{
|
||||
$min = $this->min_value;
|
||||
$max = $this->max_value;
|
||||
|
||||
if (is_null($min) && is_null($max)) {
|
||||
return '전체';
|
||||
}
|
||||
|
||||
if (is_null($min)) {
|
||||
return "~ {$max}";
|
||||
}
|
||||
|
||||
if (is_null($max)) {
|
||||
return "{$min} ~";
|
||||
}
|
||||
|
||||
return "{$min} ~ {$max}";
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
namespace App\Services\Quote;
|
||||
|
||||
use App\Models\Products\Price;
|
||||
use App\Models\Quote\QuoteFormula;
|
||||
use App\Services\Service;
|
||||
use Illuminate\Support\Collection;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* 수식 평가 서비스
|
||||
@@ -395,4 +399,139 @@ public function reset(): void
|
||||
$this->variables = [];
|
||||
$this->errors = [];
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DB 기반 수식 실행 (mng 패턴)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 범위별 수식 평가 (QuoteFormula 기반)
|
||||
*/
|
||||
public function evaluateRangeFormula(QuoteFormula $formula, array $variables = []): mixed
|
||||
{
|
||||
$conditionVar = $formula->ranges->first()?->condition_variable;
|
||||
$value = $variables[$conditionVar] ?? 0;
|
||||
|
||||
foreach ($formula->ranges as $range) {
|
||||
if ($range->isInRange($value)) {
|
||||
if ($range->result_type === 'formula') {
|
||||
return $this->evaluate($range->result_value, $variables);
|
||||
}
|
||||
|
||||
return $range->result_value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 매핑 수식 평가 (QuoteFormula 기반)
|
||||
*/
|
||||
public function evaluateMappingFormula(QuoteFormula $formula, array $variables = []): mixed
|
||||
{
|
||||
foreach ($formula->mappings as $mapping) {
|
||||
$sourceValue = $variables[$mapping->source_variable] ?? null;
|
||||
|
||||
if ($sourceValue == $mapping->source_value) {
|
||||
if ($mapping->result_type === 'formula') {
|
||||
return $this->evaluate($mapping->result_value, $variables);
|
||||
}
|
||||
|
||||
return $mapping->result_value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체 수식 실행 (카테고리 순서대로)
|
||||
*
|
||||
* @param Collection $formulasByCategory 카테고리별 수식 컬렉션
|
||||
* @param array $inputVariables 입력 변수
|
||||
* @return array ['variables' => 결과, 'items' => 품목, 'errors' => 에러]
|
||||
*/
|
||||
public function executeAll(Collection $formulasByCategory, array $inputVariables = []): array
|
||||
{
|
||||
if (! $this->tenantId()) {
|
||||
throw new RuntimeException(__('error.tenant_id_required'));
|
||||
}
|
||||
|
||||
$this->variables = $inputVariables;
|
||||
$results = [];
|
||||
$items = [];
|
||||
|
||||
foreach ($formulasByCategory as $categoryCode => $formulas) {
|
||||
foreach ($formulas as $formula) {
|
||||
$result = $this->executeFormula($formula);
|
||||
|
||||
if ($formula->output_type === QuoteFormula::OUTPUT_VARIABLE) {
|
||||
$this->variables[$formula->variable] = $result;
|
||||
$results[$formula->variable] = [
|
||||
'name' => $formula->name,
|
||||
'value' => $result,
|
||||
'category' => $formula->category->name,
|
||||
'type' => $formula->type,
|
||||
];
|
||||
} else {
|
||||
// 품목 출력
|
||||
foreach ($formula->items as $item) {
|
||||
$quantity = $this->evaluate($item->quantity_formula);
|
||||
$unitPrice = $item->unit_price_formula
|
||||
? $this->evaluate($item->unit_price_formula)
|
||||
: $this->getItemPrice($item->item_code);
|
||||
|
||||
$items[] = [
|
||||
'item_code' => $item->item_code,
|
||||
'item_name' => $item->item_name,
|
||||
'specification' => $item->specification,
|
||||
'unit' => $item->unit,
|
||||
'quantity' => $quantity,
|
||||
'unit_price' => $unitPrice,
|
||||
'total_price' => $quantity * $unitPrice,
|
||||
'formula_variable' => $formula->variable,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'variables' => $results,
|
||||
'items' => $items,
|
||||
'errors' => $this->errors,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 단일 수식 실행
|
||||
*/
|
||||
private function executeFormula(QuoteFormula $formula): mixed
|
||||
{
|
||||
return match ($formula->type) {
|
||||
QuoteFormula::TYPE_INPUT => $this->variables[$formula->variable] ??
|
||||
($formula->formula ? $this->evaluate($formula->formula) : null),
|
||||
QuoteFormula::TYPE_CALCULATION => $this->evaluate($formula->formula, $this->variables),
|
||||
QuoteFormula::TYPE_RANGE => $this->evaluateRangeFormula($formula, $this->variables),
|
||||
QuoteFormula::TYPE_MAPPING => $this->evaluateMappingFormula($formula, $this->variables),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 단가 조회
|
||||
*/
|
||||
private function getItemPrice(string $itemCode): float
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
if (! $tenantId) {
|
||||
$this->errors[] = __('error.tenant_id_required');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Price::getSalesPriceByItemCode($tenantId, $itemCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,91 +2,78 @@
|
||||
|
||||
namespace App\Services\Quote;
|
||||
|
||||
use App\Models\Products\Price;
|
||||
use App\Models\Quote\Quote;
|
||||
use App\Models\Quote\QuoteFormula;
|
||||
use App\Models\Quote\QuoteFormulaCategory;
|
||||
use App\Services\Service;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* 견적 자동산출 서비스
|
||||
*
|
||||
* 입력 파라미터(W0, H0 등)를 기반으로 견적 품목과 금액을 자동 계산합니다.
|
||||
* DB에 저장된 수식(quote_formulas)을 기반으로 견적 품목과 금액을 자동 계산합니다.
|
||||
* 제품 카테고리(스크린/철재)별 계산 로직을 지원합니다.
|
||||
*/
|
||||
class QuoteCalculationService extends Service
|
||||
{
|
||||
private ?int $tenantId = null;
|
||||
|
||||
public function __construct(
|
||||
private FormulaEvaluatorService $formulaEvaluator
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 테넌트 ID 설정
|
||||
*/
|
||||
public function setTenantId(int $tenantId): self
|
||||
{
|
||||
$this->tenantId = $tenantId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 코드로 단가 조회 (prices 테이블 연동)
|
||||
*
|
||||
* @param string $itemCode 품목 코드
|
||||
* @param float $fallback 조회 실패 시 기본값
|
||||
*/
|
||||
private function getUnitPrice(string $itemCode, float $fallback = 0): float
|
||||
{
|
||||
if (! $this->tenantId) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$price = Price::getSalesPriceByItemCode($this->tenantId, $itemCode);
|
||||
|
||||
return $price > 0 ? $price : $fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적 자동산출 실행
|
||||
*
|
||||
* @param array $inputs 입력 파라미터
|
||||
* @param string|null $productCategory 제품 카테고리
|
||||
* @param int|null $productId 제품 ID (제품별 전용 수식 조회용)
|
||||
* @return array 산출 결과
|
||||
*/
|
||||
public function calculate(array $inputs, ?string $productCategory = null): array
|
||||
public function calculate(array $inputs, ?string $productCategory = null, ?int $productId = null): array
|
||||
{
|
||||
$category = $productCategory ?? Quote::CATEGORY_SCREEN;
|
||||
|
||||
// 기본 변수 초기화
|
||||
$this->formulaEvaluator->reset();
|
||||
|
||||
// 입력값 검증 및 변수 설정
|
||||
$validatedInputs = $this->validateInputs($inputs, $category);
|
||||
$this->formulaEvaluator->setVariables($validatedInputs);
|
||||
|
||||
// 카테고리별 산출 로직 실행
|
||||
$result = match ($category) {
|
||||
Quote::CATEGORY_SCREEN => $this->calculateScreen($validatedInputs),
|
||||
Quote::CATEGORY_STEEL => $this->calculateSteel($validatedInputs),
|
||||
default => $this->calculateScreen($validatedInputs),
|
||||
};
|
||||
// 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['outputs'],
|
||||
'outputs' => $result['variables'],
|
||||
'items' => $result['items'],
|
||||
'costs' => $result['costs'],
|
||||
'errors' => $this->formulaEvaluator->getErrors(),
|
||||
'costs' => $costs,
|
||||
'errors' => $result['errors'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적 미리보기 (저장 없이 계산만)
|
||||
*/
|
||||
public function preview(array $inputs, ?string $productCategory = null): array
|
||||
public function preview(array $inputs, ?string $productCategory = null, ?int $productId = null): array
|
||||
{
|
||||
return $this->calculate($inputs, $productCategory);
|
||||
return $this->calculate($inputs, $productCategory, $productId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,8 +83,44 @@ public function recalculate(Quote $quote): array
|
||||
{
|
||||
$inputs = $quote->calculation_inputs ?? [];
|
||||
$category = $quote->product_category;
|
||||
$productId = $quote->product_id;
|
||||
|
||||
return $this->calculate($inputs, $category);
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,285 +152,16 @@ private function validateInputs(array $inputs, string $category): array
|
||||
]);
|
||||
}
|
||||
|
||||
// 추가 사용자 입력값 병합 (DB 수식에서 사용할 수 있도록)
|
||||
foreach ($inputs as $key => $value) {
|
||||
if (! isset($validated[$key])) {
|
||||
$validated[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $validated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 스크린 제품 산출
|
||||
*/
|
||||
private function calculateScreen(array $inputs): array
|
||||
{
|
||||
$w = $inputs['W0'];
|
||||
$h = $inputs['H0'];
|
||||
$qty = $inputs['QTY'];
|
||||
|
||||
// 파생 계산값
|
||||
$outputs = [];
|
||||
|
||||
// W1: 실제 폭 (케이스 마진 포함)
|
||||
$outputs['W1'] = $this->formulaEvaluator->evaluate('W0 + 100', ['W0' => $w]);
|
||||
|
||||
// H1: 실제 높이 (브라켓 마진 포함)
|
||||
$outputs['H1'] = $this->formulaEvaluator->evaluate('H0 + 150', ['H0' => $h]);
|
||||
|
||||
// 면적 (m²)
|
||||
$outputs['AREA'] = $this->formulaEvaluator->evaluate('(W1 * H1) / 1000000', [
|
||||
'W1' => $outputs['W1'],
|
||||
'H1' => $outputs['H1'],
|
||||
]);
|
||||
|
||||
// 무게 (kg) - 대략 계산
|
||||
$outputs['WEIGHT'] = $this->formulaEvaluator->evaluate('AREA * 5', [
|
||||
'AREA' => $outputs['AREA'],
|
||||
]);
|
||||
|
||||
// 모터 용량 결정 (면적 기준)
|
||||
$outputs['MOTOR_CAPACITY'] = $this->formulaEvaluator->evaluateRange($outputs['AREA'], [
|
||||
['min' => 0, 'max' => 5, 'result' => '50W'],
|
||||
['min' => 5, 'max' => 10, 'result' => '100W'],
|
||||
['min' => 10, 'max' => 20, 'result' => '200W'],
|
||||
['min' => 20, 'max' => null, 'result' => '300W'],
|
||||
], '100W');
|
||||
|
||||
// 품목 생성
|
||||
$items = $this->generateScreenItems($inputs, $outputs, $qty);
|
||||
|
||||
// 비용 계산
|
||||
$costs = $this->calculateCosts($items);
|
||||
|
||||
return [
|
||||
'outputs' => $outputs,
|
||||
'items' => $items,
|
||||
'costs' => $costs,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 철재 제품 산출
|
||||
*/
|
||||
private function calculateSteel(array $inputs): array
|
||||
{
|
||||
$w = $inputs['W0'];
|
||||
$h = $inputs['H0'];
|
||||
$qty = $inputs['QTY'];
|
||||
$thickness = $inputs['THICKNESS'];
|
||||
|
||||
// 파생 계산값
|
||||
$outputs = [];
|
||||
|
||||
// 실제 크기 (용접 마진 포함)
|
||||
$outputs['W1'] = $this->formulaEvaluator->evaluate('W0 + 50', ['W0' => $w]);
|
||||
$outputs['H1'] = $this->formulaEvaluator->evaluate('H0 + 50', ['H0' => $h]);
|
||||
|
||||
// 면적 (m²)
|
||||
$outputs['AREA'] = $this->formulaEvaluator->evaluate('(W1 * H1) / 1000000', [
|
||||
'W1' => $outputs['W1'],
|
||||
'H1' => $outputs['H1'],
|
||||
]);
|
||||
|
||||
// 중량 (kg) - 재질별 밀도 적용
|
||||
$density = $this->formulaEvaluator->evaluateMapping($inputs['MATERIAL'], [
|
||||
['source' => 'ss304', 'result' => 7.93],
|
||||
['source' => 'ss316', 'result' => 8.0],
|
||||
['source' => 'galvanized', 'result' => 7.85],
|
||||
], 7.85);
|
||||
|
||||
$outputs['WEIGHT'] = $this->formulaEvaluator->evaluate('AREA * THICKNESS * DENSITY', [
|
||||
'AREA' => $outputs['AREA'],
|
||||
'THICKNESS' => $thickness / 1000, // mm to m
|
||||
'DENSITY' => $density * 1000, // kg/m³
|
||||
]);
|
||||
|
||||
// 품목 생성
|
||||
$items = $this->generateSteelItems($inputs, $outputs, $qty);
|
||||
|
||||
// 비용 계산
|
||||
$costs = $this->calculateCosts($items);
|
||||
|
||||
return [
|
||||
'outputs' => $outputs,
|
||||
'items' => $items,
|
||||
'costs' => $costs,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 스크린 품목 생성
|
||||
*/
|
||||
private function generateScreenItems(array $inputs, array $outputs, int $qty): array
|
||||
{
|
||||
$items = [];
|
||||
|
||||
// 1. 스크린 원단
|
||||
$fabricPrice = $this->getUnitPrice('SCR-FABRIC-001', 25000);
|
||||
$items[] = [
|
||||
'item_code' => 'SCR-FABRIC-001',
|
||||
'item_name' => '스크린 원단',
|
||||
'specification' => sprintf('%.0f x %.0f mm', $outputs['W1'], $outputs['H1']),
|
||||
'unit' => 'm²',
|
||||
'base_quantity' => 1,
|
||||
'calculated_quantity' => $outputs['AREA'] * $qty,
|
||||
'unit_price' => $fabricPrice,
|
||||
'total_price' => $outputs['AREA'] * $qty * $fabricPrice,
|
||||
'formula' => 'AREA * QTY',
|
||||
'formula_category' => 'material',
|
||||
];
|
||||
|
||||
// 2. 케이스
|
||||
$casePrice = $this->getUnitPrice('SCR-CASE-001', 85000);
|
||||
$items[] = [
|
||||
'item_code' => 'SCR-CASE-001',
|
||||
'item_name' => '알루미늄 케이스',
|
||||
'specification' => sprintf('%.0f mm', $outputs['W1']),
|
||||
'unit' => 'EA',
|
||||
'base_quantity' => 1,
|
||||
'calculated_quantity' => $qty,
|
||||
'unit_price' => $casePrice,
|
||||
'total_price' => $qty * $casePrice,
|
||||
'formula' => 'QTY',
|
||||
'formula_category' => 'material',
|
||||
];
|
||||
|
||||
// 3. 모터
|
||||
$motorPrice = $this->getMotorPrice($outputs['MOTOR_CAPACITY']);
|
||||
$items[] = [
|
||||
'item_code' => 'SCR-MOTOR-001',
|
||||
'item_name' => '튜블러 모터',
|
||||
'specification' => $outputs['MOTOR_CAPACITY'],
|
||||
'unit' => 'EA',
|
||||
'base_quantity' => 1,
|
||||
'calculated_quantity' => $qty,
|
||||
'unit_price' => $motorPrice,
|
||||
'total_price' => $qty * $motorPrice,
|
||||
'formula' => 'QTY',
|
||||
'formula_category' => 'material',
|
||||
];
|
||||
|
||||
// 4. 브라켓
|
||||
$bracketPrice = $this->getUnitPrice('SCR-BRACKET-001', 15000);
|
||||
$items[] = [
|
||||
'item_code' => 'SCR-BRACKET-001',
|
||||
'item_name' => '설치 브라켓',
|
||||
'specification' => $inputs['INSTALL_TYPE'],
|
||||
'unit' => 'SET',
|
||||
'base_quantity' => 2,
|
||||
'calculated_quantity' => 2 * $qty,
|
||||
'unit_price' => $bracketPrice,
|
||||
'total_price' => 2 * $qty * $bracketPrice,
|
||||
'formula' => '2 * QTY',
|
||||
'formula_category' => 'material',
|
||||
];
|
||||
|
||||
// 5. 인건비
|
||||
$laborHours = $this->formulaEvaluator->evaluateRange($outputs['AREA'], [
|
||||
['min' => 0, 'max' => 5, 'result' => 2],
|
||||
['min' => 5, 'max' => 10, 'result' => 3],
|
||||
['min' => 10, 'max' => null, 'result' => 4],
|
||||
], 2);
|
||||
|
||||
$laborPrice = $this->getUnitPrice('LAB-INSTALL-001', 50000);
|
||||
$items[] = [
|
||||
'item_code' => 'LAB-INSTALL-001',
|
||||
'item_name' => '설치 인건비',
|
||||
'specification' => sprintf('%.1f시간', $laborHours * $qty),
|
||||
'unit' => 'HR',
|
||||
'base_quantity' => $laborHours,
|
||||
'calculated_quantity' => $laborHours * $qty,
|
||||
'unit_price' => $laborPrice,
|
||||
'total_price' => $laborHours * $qty * $laborPrice,
|
||||
'formula' => 'LABOR_HOURS * QTY',
|
||||
'formula_category' => 'labor',
|
||||
];
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 철재 품목 생성
|
||||
*/
|
||||
private function generateSteelItems(array $inputs, array $outputs, int $qty): array
|
||||
{
|
||||
$items = [];
|
||||
|
||||
// 재질별 품목코드 및 단가 조회
|
||||
$materialCode = 'STL-PLATE-'.strtoupper($inputs['MATERIAL']);
|
||||
$fallbackMaterialPrice = $this->formulaEvaluator->evaluateMapping($inputs['MATERIAL'], [
|
||||
['source' => 'ss304', 'result' => 4500],
|
||||
['source' => 'ss316', 'result' => 6500],
|
||||
['source' => 'galvanized', 'result' => 3000],
|
||||
], 4500);
|
||||
$materialPrice = $this->getUnitPrice($materialCode, $fallbackMaterialPrice);
|
||||
|
||||
// 1. 철판
|
||||
$items[] = [
|
||||
'item_code' => $materialCode,
|
||||
'item_name' => '철판 ('.$inputs['MATERIAL'].')',
|
||||
'specification' => sprintf('%.0f x %.0f x %.1f mm', $outputs['W1'], $outputs['H1'], $inputs['THICKNESS']),
|
||||
'unit' => 'kg',
|
||||
'base_quantity' => $outputs['WEIGHT'],
|
||||
'calculated_quantity' => $outputs['WEIGHT'] * $qty,
|
||||
'unit_price' => $materialPrice,
|
||||
'total_price' => $outputs['WEIGHT'] * $qty * $materialPrice,
|
||||
'formula' => 'WEIGHT * QTY * MATERIAL_PRICE',
|
||||
'formula_category' => 'material',
|
||||
];
|
||||
|
||||
// 2. 용접
|
||||
$weldLength = ($outputs['W1'] + $outputs['H1']) * 2 / 1000; // m
|
||||
$weldPrice = $this->getUnitPrice('STL-WELD-001', 15000);
|
||||
$items[] = [
|
||||
'item_code' => 'STL-WELD-001',
|
||||
'item_name' => '용접 ('.$inputs['WELDING'].')',
|
||||
'specification' => sprintf('%.2f m', $weldLength * $qty),
|
||||
'unit' => 'm',
|
||||
'base_quantity' => $weldLength,
|
||||
'calculated_quantity' => $weldLength * $qty,
|
||||
'unit_price' => $weldPrice,
|
||||
'total_price' => $weldLength * $qty * $weldPrice,
|
||||
'formula' => 'WELD_LENGTH * QTY',
|
||||
'formula_category' => 'labor',
|
||||
];
|
||||
|
||||
// 3. 표면처리
|
||||
$finishCode = 'STL-FINISH-'.strtoupper($inputs['FINISH']);
|
||||
$fallbackFinishPrice = $this->formulaEvaluator->evaluateMapping($inputs['FINISH'], [
|
||||
['source' => 'hairline', 'result' => 8000],
|
||||
['source' => 'mirror', 'result' => 15000],
|
||||
['source' => 'matte', 'result' => 5000],
|
||||
], 8000);
|
||||
$finishPrice = $this->getUnitPrice($finishCode, $fallbackFinishPrice);
|
||||
|
||||
$items[] = [
|
||||
'item_code' => $finishCode,
|
||||
'item_name' => '표면처리 ('.$inputs['FINISH'].')',
|
||||
'specification' => sprintf('%.2f m²', $outputs['AREA'] * $qty),
|
||||
'unit' => 'm²',
|
||||
'base_quantity' => $outputs['AREA'],
|
||||
'calculated_quantity' => $outputs['AREA'] * $qty,
|
||||
'unit_price' => $finishPrice,
|
||||
'total_price' => $outputs['AREA'] * $qty * $finishPrice,
|
||||
'formula' => 'AREA * QTY',
|
||||
'formula_category' => 'labor',
|
||||
];
|
||||
|
||||
// 4. 가공비
|
||||
$processPrice = $this->getUnitPrice('STL-PROCESS-001', 50000);
|
||||
$items[] = [
|
||||
'item_code' => 'STL-PROCESS-001',
|
||||
'item_name' => '가공비',
|
||||
'specification' => '절단, 벤딩, 천공',
|
||||
'unit' => 'EA',
|
||||
'base_quantity' => 1,
|
||||
'calculated_quantity' => $qty,
|
||||
'unit_price' => $processPrice,
|
||||
'total_price' => $qty * $processPrice,
|
||||
'formula' => 'QTY',
|
||||
'formula_category' => 'labor',
|
||||
];
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 비용 계산
|
||||
*/
|
||||
@@ -440,27 +194,48 @@ private function calculateCosts(array $items): array
|
||||
}
|
||||
|
||||
/**
|
||||
* 모터 단가 조회 (prices 테이블 연동)
|
||||
* 입력 스키마 반환 (프론트엔드용)
|
||||
* DB에서 input 타입 수식을 조회하여 동적으로 생성
|
||||
*/
|
||||
private function getMotorPrice(string $capacity): float
|
||||
public function getInputSchema(?string $productCategory = null, ?int $productId = null): array
|
||||
{
|
||||
// 용량별 품목코드 및 기본 단가
|
||||
$motorCode = 'SCR-MOTOR-'.$capacity;
|
||||
$fallbackPrice = match ($capacity) {
|
||||
'50W' => 120000,
|
||||
'100W' => 150000,
|
||||
'200W' => 200000,
|
||||
'300W' => 280000,
|
||||
default => 150000,
|
||||
};
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return $this->getUnitPrice($motorCode, $fallbackPrice);
|
||||
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에 수식이 없을 때 사용)
|
||||
*/
|
||||
public function getInputSchema(?string $productCategory = null): array
|
||||
private function getDefaultInputSchema(?string $productCategory = null): array
|
||||
{
|
||||
$category = $productCategory ?? Quote::CATEGORY_SCREEN;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user