refactor: products/materials 테이블 및 관련 코드 삭제
- products, materials, product_components 테이블 삭제 마이그레이션 - FK 제약조건 정리 (orders, order_items, material_receipts, lots) - 관련 Models 삭제: Product, Material, ProductComponent 등 - 관련 Controllers 삭제: ProductController, MaterialController, ProductBomItemController - 관련 Services 삭제: ProductService, MaterialService, ProductBomService - 관련 Requests, Swagger 파일 삭제 - 라우트 정리: /products, /materials 엔드포인트 제거 모든 품목 관리는 /items 엔드포인트로 통합됨 item_id_mappings 테이블에 ID 매핑 보존 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Materials;
|
||||
|
||||
use App\Models\Commons\Category;
|
||||
use App\Models\Commons\File;
|
||||
use App\Models\Commons\Tag;
|
||||
use App\Models\Qualitys\Lot;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* @mixin IdeHelperMaterial
|
||||
*/
|
||||
class Material extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'category_id',
|
||||
'name',
|
||||
'item_name',
|
||||
'specification',
|
||||
'material_code',
|
||||
'material_type',
|
||||
'unit',
|
||||
'is_inspection',
|
||||
'search_tag',
|
||||
'remarks',
|
||||
'attributes',
|
||||
'options',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'attributes' => 'array',
|
||||
'options' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
// 카테고리
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
|
||||
// 자재 입고 내역
|
||||
public function receipts()
|
||||
{
|
||||
return $this->hasMany(MaterialReceipt::class, 'material_id');
|
||||
}
|
||||
|
||||
// 로트 관리
|
||||
public function lots()
|
||||
{
|
||||
return $this->hasMany(Lot::class, 'material_id');
|
||||
}
|
||||
|
||||
// 파일 목록 (N:M, 폴리모픽)
|
||||
public function files()
|
||||
{
|
||||
return $this->morphMany(File::class, 'fileable');
|
||||
}
|
||||
|
||||
// 태그 목록 (N:M, 폴리모픽)
|
||||
public function tags()
|
||||
{
|
||||
return $this->morphToMany(Tag::class, 'taggable');
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Products;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* @mixin IdeHelperCommonCode
|
||||
*/
|
||||
class CommonCode extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'common_codes';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'code_group',
|
||||
'code',
|
||||
'name',
|
||||
'parent_id',
|
||||
'attributes',
|
||||
'description',
|
||||
'is_active',
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'attributes' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
// 관계: 상위 코드
|
||||
public function parent()
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_id');
|
||||
}
|
||||
|
||||
// 관계: 하위 코드들
|
||||
public function children()
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_id');
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Products;
|
||||
|
||||
use App\Models\Commons\Tag;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* @mixin IdeHelperPart
|
||||
*/
|
||||
class Part extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = ['tenant_id', 'code', 'name', 'category_id', 'part_type_id', 'unit', 'attributes', 'description', 'is_active'];
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(CommonCode::class, 'category_id');
|
||||
}
|
||||
|
||||
public function partType()
|
||||
{
|
||||
return $this->belongsTo(CommonCode::class, 'part_type_id');
|
||||
}
|
||||
|
||||
// 태그 목록 (N:M, 폴리모픽)
|
||||
public function tags()
|
||||
{
|
||||
return $this->morphToMany(Tag::class, 'taggable');
|
||||
}
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Products;
|
||||
|
||||
use App\Models\Materials\Material;
|
||||
use App\Models\Orders\ClientGroup;
|
||||
use App\Traits\BelongsToTenant;
|
||||
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 $item_type_code
|
||||
* @property int $item_id
|
||||
* @property int|null $client_group_id
|
||||
* @property float|null $purchase_price
|
||||
* @property float|null $processing_cost
|
||||
* @property float|null $loss_rate
|
||||
* @property float|null $margin_rate
|
||||
* @property float|null $sales_price
|
||||
* @property string $rounding_rule
|
||||
* @property int $rounding_unit
|
||||
* @property string|null $supplier
|
||||
* @property \Carbon\Carbon $effective_from
|
||||
* @property \Carbon\Carbon|null $effective_to
|
||||
* @property string|null $note
|
||||
* @property string $status
|
||||
* @property bool $is_final
|
||||
* @property \Carbon\Carbon|null $finalized_at
|
||||
* @property int|null $finalized_by
|
||||
*/
|
||||
class Price extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'item_type_code',
|
||||
'item_id',
|
||||
'client_group_id',
|
||||
'purchase_price',
|
||||
'processing_cost',
|
||||
'loss_rate',
|
||||
'margin_rate',
|
||||
'sales_price',
|
||||
'rounding_rule',
|
||||
'rounding_unit',
|
||||
'supplier',
|
||||
'effective_from',
|
||||
'effective_to',
|
||||
'note',
|
||||
'status',
|
||||
'is_final',
|
||||
'finalized_at',
|
||||
'finalized_by',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'purchase_price' => 'decimal:4',
|
||||
'processing_cost' => 'decimal:4',
|
||||
'loss_rate' => 'decimal:2',
|
||||
'margin_rate' => 'decimal:2',
|
||||
'sales_price' => 'decimal:4',
|
||||
'rounding_unit' => 'integer',
|
||||
'effective_from' => 'date',
|
||||
'effective_to' => 'date',
|
||||
'is_final' => 'boolean',
|
||||
'finalized_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 고객 그룹 관계
|
||||
*/
|
||||
public function clientGroup(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ClientGroup::class, 'client_group_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 리비전 이력 관계
|
||||
*/
|
||||
public function revisions(): HasMany
|
||||
{
|
||||
return $this->hasMany(PriceRevision::class, 'price_id')->orderBy('revision_number', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 관계 (Polymorphic - item_type_code에 따라)
|
||||
*/
|
||||
public function item()
|
||||
{
|
||||
if ($this->item_type_code === 'PRODUCT') {
|
||||
return $this->belongsTo(Product::class, 'item_id');
|
||||
} elseif ($this->item_type_code === 'MATERIAL') {
|
||||
return $this->belongsTo(Material::class, 'item_id');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 제품 관계 (item_type_code = PRODUCT인 경우)
|
||||
*/
|
||||
public function product(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Product::class, 'item_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 자재 관계 (item_type_code = MATERIAL인 경우)
|
||||
*/
|
||||
public function material(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Material::class, 'item_id');
|
||||
}
|
||||
|
||||
// ========== 스코프 ==========
|
||||
|
||||
/**
|
||||
* 특정 품목 필터
|
||||
*/
|
||||
public function scopeForItem($query, string $itemType, int $itemId)
|
||||
{
|
||||
return $query->where('item_type_code', $itemType)
|
||||
->where('item_id', $itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 고객 그룹 필터
|
||||
*/
|
||||
public function scopeForClientGroup($query, ?int $clientGroupId)
|
||||
{
|
||||
return $query->where('client_group_id', $clientGroupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 일자에 유효한 단가
|
||||
*/
|
||||
public function scopeValidAt($query, $date)
|
||||
{
|
||||
return $query->where('effective_from', '<=', $date)
|
||||
->where(function ($q) use ($date) {
|
||||
$q->whereNull('effective_to')
|
||||
->orWhere('effective_to', '>=', $date);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 필터
|
||||
*/
|
||||
public function scopeStatus($query, string $status)
|
||||
{
|
||||
return $query->where('status', $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성 단가만
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', 'active');
|
||||
}
|
||||
|
||||
/**
|
||||
* 확정된 단가만
|
||||
*/
|
||||
public function scopeFinalized($query)
|
||||
{
|
||||
return $query->where('is_final', true);
|
||||
}
|
||||
|
||||
// ========== 계산 메서드 ==========
|
||||
|
||||
/**
|
||||
* 총원가 계산
|
||||
* 총원가 = (매입단가 + 가공비) × (1 + LOSS율/100)
|
||||
*/
|
||||
public function calculateTotalCost(): float
|
||||
{
|
||||
$baseCost = ($this->purchase_price ?? 0) + ($this->processing_cost ?? 0);
|
||||
$lossMultiplier = 1 + (($this->loss_rate ?? 0) / 100);
|
||||
|
||||
return $baseCost * $lossMultiplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* 판매단가 계산 (마진율 기반)
|
||||
* 판매단가 = 반올림(총원가 × (1 + 마진율/100), 반올림단위, 반올림규칙)
|
||||
*/
|
||||
public function calculateSalesPrice(): float
|
||||
{
|
||||
$totalCost = $this->calculateTotalCost();
|
||||
$marginMultiplier = 1 + (($this->margin_rate ?? 0) / 100);
|
||||
$rawPrice = $totalCost * $marginMultiplier;
|
||||
|
||||
return $this->applyRounding($rawPrice);
|
||||
}
|
||||
|
||||
/**
|
||||
* 반올림 적용
|
||||
*/
|
||||
private function applyRounding(float $value): float
|
||||
{
|
||||
$unit = $this->rounding_unit ?: 1;
|
||||
|
||||
return match ($this->rounding_rule) {
|
||||
'ceil' => ceil($value / $unit) * $unit,
|
||||
'floor' => floor($value / $unit) * $unit,
|
||||
default => round($value / $unit) * $unit, // 'round'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 확정 가능 여부
|
||||
*/
|
||||
public function canFinalize(): bool
|
||||
{
|
||||
return ! $this->is_final && in_array($this->status, ['draft', 'active']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정 가능 여부
|
||||
*/
|
||||
public function canEdit(): bool
|
||||
{
|
||||
return ! $this->is_final;
|
||||
}
|
||||
|
||||
/**
|
||||
* 스냅샷 생성 (리비전용)
|
||||
*/
|
||||
public function toSnapshot(): array
|
||||
{
|
||||
return [
|
||||
'purchase_price' => $this->purchase_price,
|
||||
'processing_cost' => $this->processing_cost,
|
||||
'loss_rate' => $this->loss_rate,
|
||||
'margin_rate' => $this->margin_rate,
|
||||
'sales_price' => $this->sales_price,
|
||||
'rounding_rule' => $this->rounding_rule,
|
||||
'rounding_unit' => $this->rounding_unit,
|
||||
'supplier' => $this->supplier,
|
||||
'effective_from' => $this->effective_from?->format('Y-m-d'),
|
||||
'effective_to' => $this->effective_to?->format('Y-m-d'),
|
||||
'status' => $this->status,
|
||||
'is_final' => $this->is_final,
|
||||
'note' => $this->note,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Products;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 단가 변경 이력 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property int $price_id
|
||||
* @property int $revision_number
|
||||
* @property \Carbon\Carbon $changed_at
|
||||
* @property int $changed_by
|
||||
* @property string|null $change_reason
|
||||
* @property array|null $before_snapshot
|
||||
* @property array $after_snapshot
|
||||
*/
|
||||
class PriceRevision extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'price_id',
|
||||
'revision_number',
|
||||
'changed_at',
|
||||
'changed_by',
|
||||
'change_reason',
|
||||
'before_snapshot',
|
||||
'after_snapshot',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'revision_number' => 'integer',
|
||||
'changed_at' => 'datetime',
|
||||
'before_snapshot' => 'array',
|
||||
'after_snapshot' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* 단가 관계
|
||||
*/
|
||||
public function price(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Price::class, 'price_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 변경자 관계
|
||||
*/
|
||||
public function changedByUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Members\User::class, 'changed_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 변경된 필드 목록 추출
|
||||
*/
|
||||
public function getChangedFields(): array
|
||||
{
|
||||
if (! $this->before_snapshot) {
|
||||
return array_keys($this->after_snapshot ?? []);
|
||||
}
|
||||
|
||||
$changed = [];
|
||||
foreach ($this->after_snapshot as $key => $newValue) {
|
||||
$oldValue = $this->before_snapshot[$key] ?? null;
|
||||
if ($oldValue !== $newValue) {
|
||||
$changed[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
return $changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 필드의 이전/이후 값
|
||||
*/
|
||||
public function getFieldChange(string $field): array
|
||||
{
|
||||
return [
|
||||
'before' => $this->before_snapshot[$field] ?? null,
|
||||
'after' => $this->after_snapshot[$field] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Products;
|
||||
|
||||
use App\Models\Commons\Category;
|
||||
use App\Models\Commons\File;
|
||||
use App\Models\Commons\Tag;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Product extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id', 'code', 'name', 'unit', 'category_id',
|
||||
'product_type', // 라벨/분류용
|
||||
'attributes', 'attributes_archive', 'options', 'bom', 'description',
|
||||
'is_sellable', 'is_purchasable', 'is_producible',
|
||||
// 하이브리드 구조: 최소 고정 필드
|
||||
'safety_stock', 'lead_time', 'is_variable_size',
|
||||
'product_category', 'part_type',
|
||||
// 파일 필드
|
||||
'bending_diagram', 'bending_details',
|
||||
'specification_file', 'specification_file_name',
|
||||
'certification_file', 'certification_file_name',
|
||||
'certification_number', 'certification_start_date', 'certification_end_date',
|
||||
'created_by', 'updated_by', 'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'attributes' => 'array',
|
||||
'attributes_archive' => 'array',
|
||||
'options' => 'array',
|
||||
'bom' => 'array',
|
||||
'bending_details' => 'array',
|
||||
'certification_start_date' => 'date',
|
||||
'certification_end_date' => 'date',
|
||||
'is_sellable' => 'boolean',
|
||||
'is_purchasable' => 'boolean',
|
||||
'is_producible' => 'boolean',
|
||||
'is_variable_size' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
// 분류
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class, 'category_id');
|
||||
}
|
||||
|
||||
// BOM (자기참조) — 라인 모델 경유
|
||||
public function componentLines()
|
||||
{
|
||||
return $this->hasMany(ProductComponent::class, 'parent_product_id')->orderBy('sort_order');
|
||||
}
|
||||
|
||||
// 라인들
|
||||
public function parentLines()
|
||||
{
|
||||
return $this->hasMany(ProductComponent::class, 'child_product_id');
|
||||
} // 나를 쓰는 상위 라인들
|
||||
|
||||
// 편의: 직접 children/parents 제품에 접근
|
||||
public function children()
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
self::class, 'product_components', 'parent_product_id', 'child_product_id'
|
||||
)->withPivot(['quantity', 'sort_order', 'is_default'])
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function parents()
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
self::class, 'product_components', 'child_product_id', 'parent_product_id'
|
||||
)->withPivot(['quantity', 'sort_order', 'is_default'])
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
// 파일 / 태그 (폴리모픽)
|
||||
public function files()
|
||||
{
|
||||
return $this->morphMany(File::class, 'fileable');
|
||||
}
|
||||
|
||||
public function tags()
|
||||
{
|
||||
return $this->morphToMany(Tag::class, 'taggable');
|
||||
}
|
||||
|
||||
// 스코프
|
||||
public function scopeType($q, string $type)
|
||||
{
|
||||
return $q->where('product_type', $type);
|
||||
}
|
||||
|
||||
public function scopeSellable($q)
|
||||
{
|
||||
return $q->where('is_sellable', 1);
|
||||
}
|
||||
|
||||
public function scopePurchasable($q)
|
||||
{
|
||||
return $q->where('is_purchasable', 1);
|
||||
}
|
||||
|
||||
public function scopeProducible($q)
|
||||
{
|
||||
return $q->where('is_producible', 1);
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Products;
|
||||
|
||||
use App\Models\Materials\Material;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class ProductComponent extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'product_components';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'parent_product_id',
|
||||
'category_id',
|
||||
'category_name',
|
||||
'ref_type',
|
||||
'ref_id',
|
||||
'quantity',
|
||||
'sort_order',
|
||||
// 하이브리드 구조: 고정 필드 (범용 BOM 계산)
|
||||
'quantity_formula',
|
||||
'condition',
|
||||
// 동적 필드 (테넌트별 커스텀)
|
||||
'attributes',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quantity' => 'decimal:6',
|
||||
'attributes' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* 상위 제품 (모델/제품)
|
||||
*/
|
||||
public function parentProduct()
|
||||
{
|
||||
return $this->belongsTo(Product::class, 'parent_product_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 참조된 제품 또는 자재를 동적으로 가져오기
|
||||
* ref_type에 따라 Product 또는 Material을 반환
|
||||
*/
|
||||
public function referencedItem()
|
||||
{
|
||||
if ($this->ref_type === 'PRODUCT') {
|
||||
return $this->belongsTo(Product::class, 'ref_id');
|
||||
} elseif ($this->ref_type === 'MATERIAL') {
|
||||
return $this->belongsTo(Material::class, 'ref_id');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 하위 제품 (ref_type = PRODUCT일 때만)
|
||||
*/
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class, 'ref_id')
|
||||
->where('ref_type', 'PRODUCT');
|
||||
}
|
||||
|
||||
/**
|
||||
* 하위 자재 (ref_type = MATERIAL일 때만)
|
||||
*/
|
||||
public function material()
|
||||
{
|
||||
return $this->belongsTo(Material::class, 'ref_id')
|
||||
->where('ref_type', 'MATERIAL');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------
|
||||
// 🔎 Query Scopes
|
||||
// ---------------------------------------------------
|
||||
|
||||
/**
|
||||
* 제품 BOM 아이템만
|
||||
*/
|
||||
public function scopeProducts($query)
|
||||
{
|
||||
return $query->where('ref_type', 'PRODUCT');
|
||||
}
|
||||
|
||||
/**
|
||||
* 자재 BOM 아이템만
|
||||
*/
|
||||
public function scopeMaterials($query)
|
||||
{
|
||||
return $query->where('ref_type', 'MATERIAL');
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 상위 제품의 BOM
|
||||
*/
|
||||
public function scopeForParent($query, int $parentProductId)
|
||||
{
|
||||
return $query->where('parent_product_id', $parentProductId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user