feat: Items 테이블 통합 마이그레이션 Phase 0-5 구현

## 주요 변경사항
- Phase 0: 비표준 item_type 데이터 정규화 마이그레이션
- Phase 1.1: items 테이블 생성 (products + materials 통합)
- Phase 1.2: item_details 테이블 생성 (1:1 확장 필드)
- Phase 1.3: 데이터 이관 + item_id_mappings 테이블 생성
- Phase 3: item_pages.source_table 업데이트
- Phase 5: 참조 테이블 마이그레이션 (product_components, orders 등)

## 신규 파일
- app/Models/Items/Item.php - 통합 아이템 모델
- app/Models/Items/ItemDetail.php - 1:1 확장 필드 모델
- app/Services/ItemService.php - 통합 서비스 클래스

## 수정 파일
- ItemPage.php - items 테이블 지원 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-13 15:41:30 +09:00
parent aa9746ae2f
commit 80281e65b7
11 changed files with 1660 additions and 4 deletions

View File

@@ -103,15 +103,25 @@ public function allRelationships()
public function getTargetModelClass(): ?string
{
$mapping = [
'products' => \App\Models\Product::class,
'materials' => \App\Models\Material::class,
'items' => \App\Models\Items\Item::class,
// 하위 호환성 (마이그레이션 완료 전까지)
'products' => \App\Models\Products\Product::class,
'materials' => \App\Models\Materials\Material::class,
];
return $mapping[$this->source_table] ?? null;
}
/**
* 제품 페이지인지 확인
* 통합 품목 페이지인지 확인
*/
public function isItemPage(): bool
{
return $this->source_table === 'items';
}
/**
* 제품 페이지인지 확인 (하위 호환성)
*/
public function isProductPage(): bool
{
@@ -119,10 +129,26 @@ public function isProductPage(): bool
}
/**
* 자재 페이지인지 확인
* 자재 페이지인지 확인 (하위 호환성)
*/
public function isMaterialPage(): bool
{
return $this->source_table === 'materials';
}
/**
* Product 타입 품목인지 확인 (items 테이블 기준)
*/
public function isProductType(): bool
{
return in_array($this->item_type, ['FG', 'PT']);
}
/**
* Material 타입 품목인지 확인 (items 테이블 기준)
*/
public function isMaterialType(): bool
{
return in_array($this->item_type, ['SM', 'RM', 'CS']);
}
}

198
app/Models/Items/Item.php Normal file
View File

@@ -0,0 +1,198 @@
<?php
namespace App\Models\Items;
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;
/**
* 통합 품목 모델
*
* products + materials를 통합한 단일 품목 테이블
* item_type: FG(완제품), PT(부품), SM(부자재), RM(원자재), CS(소모품)
*/
class Item extends Model
{
use BelongsToTenant, ModelTrait, SoftDeletes;
protected $fillable = [
'tenant_id',
'item_type',
'code',
'name',
'unit',
'category_id',
'bom',
'attributes',
'attributes_archive',
'options',
'description',
'is_active',
'created_by',
'updated_by',
];
protected $casts = [
'bom' => 'array',
'attributes' => 'array',
'attributes_archive' => 'array',
'options' => 'array',
'is_active' => 'boolean',
];
protected $hidden = [
'deleted_at',
];
/**
* item_type 상수
*/
public const TYPE_FINISHED_GOODS = 'FG'; // 완제품
public const TYPE_PARTS = 'PT'; // 부품
public const TYPE_SUB_MATERIALS = 'SM'; // 부자재
public const TYPE_RAW_MATERIALS = 'RM'; // 원자재
public const TYPE_CONSUMABLES = 'CS'; // 소모품
/**
* Products 타입 (FG, PT)
*/
public const PRODUCT_TYPES = ['FG', 'PT'];
/**
* Materials 타입 (SM, RM, CS)
*/
public const MATERIAL_TYPES = ['SM', 'RM', 'CS'];
// ──────────────────────────────────────────────────────────────
// 관계
// ──────────────────────────────────────────────────────────────
/**
* 상세 정보 (1:1)
*/
public function details()
{
return $this->hasOne(ItemDetail::class);
}
/**
* 카테고리
*/
public function category()
{
return $this->belongsTo(Category::class, 'category_id');
}
/**
* BOM 자식 품목들 (JSON bom 필드 기반)
* 주의: 이 관계는 bom JSON에서 child_item_id를 추출하여 조회
*/
public function bomChildren()
{
$childIds = collect($this->bom ?? [])->pluck('child_item_id')->filter()->toArray();
return self::whereIn('id', $childIds);
}
/**
* 파일 (폴리모픽)
*/
public function files()
{
return $this->morphMany(File::class, 'fileable');
}
/**
* 태그 (폴리모픽)
*/
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
// ──────────────────────────────────────────────────────────────
// 스코프
// ──────────────────────────────────────────────────────────────
/**
* 특정 타입 필터
*/
public function scopeType($query, string $type)
{
return $query->where('item_type', strtoupper($type));
}
/**
* Products 타입만 (FG, PT)
*/
public function scopeProducts($query)
{
return $query->whereIn('item_type', self::PRODUCT_TYPES);
}
/**
* Materials 타입만 (SM, RM, CS)
*/
public function scopeMaterials($query)
{
return $query->whereIn('item_type', self::MATERIAL_TYPES);
}
/**
* 활성 품목만
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
// ──────────────────────────────────────────────────────────────
// 헬퍼 메서드
// ──────────────────────────────────────────────────────────────
/**
* Product 타입인지 확인
*/
public function isProduct(): bool
{
return in_array($this->item_type, self::PRODUCT_TYPES);
}
/**
* Material 타입인지 확인
*/
public function isMaterial(): bool
{
return in_array($this->item_type, self::MATERIAL_TYPES);
}
/**
* BOM 자식 품목 ID 목록 추출
*/
public function getBomChildIds(): array
{
return collect($this->bom ?? [])->pluck('child_item_id')->filter()->toArray();
}
/**
* BOM 자식 품목들 조회 (Eager Loading 최적화)
*/
public function loadBomChildren()
{
$childIds = $this->getBomChildIds();
if (empty($childIds)) {
return collect();
}
return self::whereIn('id', $childIds)->get()->keyBy('id');
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace App\Models\Items;
use Illuminate\Database\Eloquent\Model;
/**
* 품목 상세 정보 모델
*
* items 테이블의 확장 필드 (1:1 관계)
* - Products 전용 필드 (is_sellable, is_purchasable 등)
* - Materials 전용 필드 (is_inspection 등)
*/
class ItemDetail extends Model
{
protected $fillable = [
'item_id',
// Products 전용
'is_sellable',
'is_purchasable',
'is_producible',
'safety_stock',
'lead_time',
'is_variable_size',
'product_category',
'part_type',
// 파일 필드 (Products)
'bending_diagram',
'bending_details',
'specification_file',
'specification_file_name',
'certification_file',
'certification_file_name',
'certification_number',
'certification_start_date',
'certification_end_date',
// Materials 전용
'is_inspection',
'item_name',
'specification',
'search_tag',
'remarks',
];
protected $casts = [
'is_sellable' => 'boolean',
'is_purchasable' => 'boolean',
'is_producible' => 'boolean',
'is_variable_size' => 'boolean',
'bending_details' => 'array',
'certification_start_date' => 'date',
'certification_end_date' => 'date',
];
/**
* 품목 (부모)
*/
public function item()
{
return $this->belongsTo(Item::class);
}
// ──────────────────────────────────────────────────────────────
// Products 전용 헬퍼
// ──────────────────────────────────────────────────────────────
/**
* 판매 가능 여부
*/
public function isSellable(): bool
{
return $this->is_sellable ?? false;
}
/**
* 구매 가능 여부
*/
public function isPurchasable(): bool
{
return $this->is_purchasable ?? false;
}
/**
* 생산 가능 여부
*/
public function isProducible(): bool
{
return $this->is_producible ?? false;
}
/**
* 인증서 유효 여부
*/
public function isCertificationValid(): bool
{
if (! $this->certification_end_date) {
return false;
}
return $this->certification_end_date->isFuture();
}
// ──────────────────────────────────────────────────────────────
// Materials 전용 헬퍼
// ──────────────────────────────────────────────────────────────
/**
* 검사 필요 여부
*/
public function requiresInspection(): bool
{
return $this->is_inspection === 'Y';
}
}