feat: BP-MES Phase 1 - products/product_components 테이블 확장

- products 테이블에 33개 필드 추가
  - 공통: is_active, margin_rate, costs, safety_stock, lead_time, is_variable_size
  - FG 전용: product_category, lot_abbreviation, note
  - PT 전용: part_type, part_usage, installation_type, assembly_type,
    side_spec_width, side_spec_height, assembly_length,
    guide_rail_model_type, guide_rail_model
  - 절곡품: bending_diagram, bending_details, material, length, bending_length
  - 인증: certification_number, start/end_date, specification/certification files
  - 동적 확장: options (JSON)

- product_components 테이블에 5개 필드 추가
  - 수식 계산: quantity_formula
  - 조건부 BOM: condition
  - 절곡품: is_bending, bending_diagram, bending_details

- Product/ProductComponent 모델 fillable/casts 업데이트
- 인덱스 추가: is_active, product_category, part_type, part_usage, is_bending
This commit is contained in:
2025-11-14 09:07:33 +09:00
parent 287b39515b
commit d5bfb24ef9
5 changed files with 507 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
# 논리적 데이터베이스 관계 문서
> **자동 생성**: 2025-11-11 11:24:13
> **자동 생성**: 2025-11-14 08:41:17
> **소스**: Eloquent 모델 관계 분석
## 📊 모델별 관계 현황

View File

@@ -19,6 +19,23 @@ class Product extends Model
'product_type', // 라벨/분류용
'attributes', 'description',
'is_sellable', 'is_purchasable', 'is_producible', 'is_active',
// BP-MES: 공통 필드
'margin_rate', 'processing_cost', 'labor_cost', 'install_cost',
'safety_stock', 'lead_time', 'is_variable_size',
// BP-MES: FG 전용
'product_category', 'lot_abbreviation', 'note',
// BP-MES: PT 전용
'part_type', 'part_usage', 'installation_type', 'assembly_type',
'side_spec_width', 'side_spec_height', 'assembly_length',
'guide_rail_model_type', 'guide_rail_model',
// BP-MES: 절곡품
'bending_diagram', 'bending_details', 'material', 'length', 'bending_length',
// BP-MES: 인증
'certification_number', 'certification_start_date', 'certification_end_date',
'specification_file', 'specification_file_name',
'certification_file', 'certification_file_name',
// BP-MES: 동적 확장
'options',
'created_by', 'updated_by',
];
@@ -28,6 +45,14 @@ class Product extends Model
'is_purchasable' => 'boolean',
'is_producible' => 'boolean',
'is_active' => 'boolean',
// BP-MES: 추가 boolean 필드
'is_variable_size' => 'boolean',
// BP-MES: JSON 필드
'bending_details' => 'array',
'options' => 'array',
// BP-MES: Date 필드
'certification_start_date' => 'date',
'certification_end_date' => 'date',
];
protected $hidden = [

View File

@@ -23,6 +23,13 @@ class ProductComponent extends Model
'ref_id',
'quantity',
'sort_order',
// BP-MES: 수식 계산 및 조건부 BOM
'quantity_formula',
'condition',
// BP-MES: 절곡품
'is_bending',
'bending_diagram',
'bending_details',
'created_by',
'updated_by',
];
@@ -32,6 +39,9 @@ class ProductComponent extends Model
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
// BP-MES: 추가 타입 캐스팅
'is_bending' => 'boolean',
'bending_details' => 'array',
];
protected $hidden = [

View File

@@ -0,0 +1,367 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* BP-MES (품목 기준관리 중심 MES/ERP) Phase 1
* products 테이블 확장: ItemMaster 구조 지원
*
* 추가 필드:
* - 공통: 가격/비용/재고/리드타임 (8개)
* - FG(제품) 전용: 제품 분류/로트/노트 (3개)
* - PT(부품) 전용: 부품 타입/용도/조립 정보/가이드레일 (9개)
* - 절곡품: 전개도/치수 정보 (5개)
* - 인증: 인증서/시방서 파일 관리 (7개)
* - 동적 확장: JSON 옵션 (1개)
*
* 총 33개 필드 추가
*
* @see /claudedocs/mes/00_baseline/BACKEND_DEVELOPMENT_ROADMAP_V2.md
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('products', function (Blueprint $table) {
// ================================================
// 공통 필드 (8개)
// ================================================
if (! Schema::hasColumn('products', 'is_active')) {
$table->boolean('is_active')
->default(true)
->after('product_type')
->comment('활성 상태');
}
if (! Schema::hasColumn('products', 'margin_rate')) {
$table->decimal('margin_rate', 5, 2)
->nullable()
->after('is_active')
->comment('마진율 (%)');
}
if (! Schema::hasColumn('products', 'processing_cost')) {
$table->decimal('processing_cost', 10, 2)
->nullable()
->after('margin_rate')
->comment('가공비');
}
if (! Schema::hasColumn('products', 'labor_cost')) {
$table->decimal('labor_cost', 10, 2)
->nullable()
->after('processing_cost')
->comment('인건비');
}
if (! Schema::hasColumn('products', 'install_cost')) {
$table->decimal('install_cost', 10, 2)
->nullable()
->after('labor_cost')
->comment('설치비');
}
if (! Schema::hasColumn('products', 'safety_stock')) {
$table->integer('safety_stock')
->nullable()
->after('install_cost')
->comment('안전 재고');
}
if (! Schema::hasColumn('products', 'lead_time')) {
$table->integer('lead_time')
->nullable()
->after('safety_stock')
->comment('리드타임 (일)');
}
if (! Schema::hasColumn('products', 'is_variable_size')) {
$table->boolean('is_variable_size')
->default(false)
->after('lead_time')
->comment('가변 치수 여부');
}
// ================================================
// FG (제품) 전용 필드 (3개)
// ================================================
if (! Schema::hasColumn('products', 'product_category')) {
$table->string('product_category', 20)
->nullable()
->after('is_variable_size')
->comment('제품 분류: SCREEN, STEEL 등');
}
if (! Schema::hasColumn('products', 'lot_abbreviation')) {
$table->string('lot_abbreviation', 10)
->nullable()
->after('product_category')
->comment('로트 약어 (예: KD)');
}
if (! Schema::hasColumn('products', 'note')) {
$table->text('note')
->nullable()
->after('lot_abbreviation')
->comment('비고');
}
// ================================================
// PT (부품) 전용 필드 (9개)
// ================================================
if (! Schema::hasColumn('products', 'part_type')) {
$table->string('part_type', 20)
->nullable()
->after('note')
->comment('부품 타입: ASSEMBLY, BENDING, PURCHASED');
}
if (! Schema::hasColumn('products', 'part_usage')) {
$table->string('part_usage', 30)
->nullable()
->after('part_type')
->comment('부품 용도: GUIDE_RAIL, BOTTOM_FINISH, CASE, DOOR, BRACKET, GENERAL');
}
if (! Schema::hasColumn('products', 'installation_type')) {
$table->string('installation_type', 20)
->nullable()
->after('part_usage')
->comment('설치 타입');
}
if (! Schema::hasColumn('products', 'assembly_type')) {
$table->string('assembly_type', 20)
->nullable()
->after('installation_type')
->comment('조립 타입');
}
if (! Schema::hasColumn('products', 'side_spec_width')) {
$table->string('side_spec_width', 20)
->nullable()
->after('assembly_type')
->comment('측면 규격 폭');
}
if (! Schema::hasColumn('products', 'side_spec_height')) {
$table->string('side_spec_height', 20)
->nullable()
->after('side_spec_width')
->comment('측면 규격 높이');
}
if (! Schema::hasColumn('products', 'assembly_length')) {
$table->string('assembly_length', 20)
->nullable()
->after('side_spec_height')
->comment('조립 길이');
}
if (! Schema::hasColumn('products', 'guide_rail_model_type')) {
$table->string('guide_rail_model_type', 50)
->nullable()
->after('assembly_length')
->comment('가이드레일 모델 타입');
}
if (! Schema::hasColumn('products', 'guide_rail_model')) {
$table->string('guide_rail_model', 50)
->nullable()
->after('guide_rail_model_type')
->comment('가이드레일 모델');
}
// ================================================
// 절곡품 필드 (5개)
// ================================================
if (! Schema::hasColumn('products', 'bending_diagram')) {
$table->string('bending_diagram', 255)
->nullable()
->after('guide_rail_model')
->comment('절곡 전개도 이미지 URL');
}
if (! Schema::hasColumn('products', 'bending_details')) {
$table->json('bending_details')
->nullable()
->after('bending_diagram')
->comment('절곡 전개도 상세 데이터 (BendingDetail[])');
}
if (! Schema::hasColumn('products', 'material')) {
$table->string('material', 50)
->nullable()
->after('bending_details')
->comment('재질');
}
if (! Schema::hasColumn('products', 'length')) {
$table->string('length', 20)
->nullable()
->after('material')
->comment('길이');
}
if (! Schema::hasColumn('products', 'bending_length')) {
$table->string('bending_length', 20)
->nullable()
->after('length')
->comment('절곡 길이');
}
// ================================================
// 인증 정보 필드 (7개)
// ================================================
if (! Schema::hasColumn('products', 'certification_number')) {
$table->string('certification_number', 50)
->nullable()
->after('bending_length')
->comment('인증 번호');
}
if (! Schema::hasColumn('products', 'certification_start_date')) {
$table->date('certification_start_date')
->nullable()
->after('certification_number')
->comment('인증 시작일');
}
if (! Schema::hasColumn('products', 'certification_end_date')) {
$table->date('certification_end_date')
->nullable()
->after('certification_start_date')
->comment('인증 종료일');
}
if (! Schema::hasColumn('products', 'specification_file')) {
$table->string('specification_file', 255)
->nullable()
->after('certification_end_date')
->comment('시방서 파일 경로');
}
if (! Schema::hasColumn('products', 'specification_file_name')) {
$table->string('specification_file_name', 255)
->nullable()
->after('specification_file')
->comment('시방서 파일명');
}
if (! Schema::hasColumn('products', 'certification_file')) {
$table->string('certification_file', 255)
->nullable()
->after('specification_file_name')
->comment('인증서 파일 경로');
}
if (! Schema::hasColumn('products', 'certification_file_name')) {
$table->string('certification_file_name', 255)
->nullable()
->after('certification_file')
->comment('인증서 파일명');
}
// ================================================
// 동적 확장 필드 (1개)
// ================================================
if (! Schema::hasColumn('products', 'options')) {
$table->json('options')
->nullable()
->after('certification_file_name')
->comment('동적 옵션 데이터 (JSON)');
}
// ================================================
// 인덱스 추가 (조회 최적화)
// ================================================
// is_active 인덱스
$hasIsActiveIndex = collect(\DB::select('SHOW INDEX FROM `products`'))
->contains(fn ($r) => $r->Key_name === 'idx_products_is_active');
if (! $hasIsActiveIndex && Schema::hasColumn('products', 'is_active')) {
$table->index('is_active', 'idx_products_is_active');
}
// product_category 인덱스
$hasProductCategoryIndex = collect(\DB::select('SHOW INDEX FROM `products`'))
->contains(fn ($r) => $r->Key_name === 'idx_products_product_category');
if (! $hasProductCategoryIndex && Schema::hasColumn('products', 'product_category')) {
$table->index('product_category', 'idx_products_product_category');
}
// part_type 인덱스
$hasPartTypeIndex = collect(\DB::select('SHOW INDEX FROM `products`'))
->contains(fn ($r) => $r->Key_name === 'idx_products_part_type');
if (! $hasPartTypeIndex && Schema::hasColumn('products', 'part_type')) {
$table->index('part_type', 'idx_products_part_type');
}
// part_usage 인덱스
$hasPartUsageIndex = collect(\DB::select('SHOW INDEX FROM `products`'))
->contains(fn ($r) => $r->Key_name === 'idx_products_part_usage');
if (! $hasPartUsageIndex && Schema::hasColumn('products', 'part_usage')) {
$table->index('part_usage', 'idx_products_part_usage');
}
});
}
public function down(): void
{
Schema::table('products', function (Blueprint $table) {
// 인덱스 제거
$indexes = ['idx_products_is_active', 'idx_products_product_category', 'idx_products_part_type', 'idx_products_part_usage'];
foreach ($indexes as $indexName) {
$hasIndex = collect(\DB::select('SHOW INDEX FROM `products`'))
->contains(fn ($r) => $r->Key_name === $indexName);
if ($hasIndex) {
$table->dropIndex($indexName);
}
}
// 컬럼 제거 (역순)
$columns = [
'options',
'certification_file_name',
'certification_file',
'specification_file_name',
'specification_file',
'certification_end_date',
'certification_start_date',
'certification_number',
'bending_length',
'length',
'material',
'bending_details',
'bending_diagram',
'guide_rail_model',
'guide_rail_model_type',
'assembly_length',
'side_spec_height',
'side_spec_width',
'assembly_type',
'installation_type',
'part_usage',
'part_type',
'note',
'lot_abbreviation',
'product_category',
'is_variable_size',
'lead_time',
'safety_stock',
'install_cost',
'labor_cost',
'processing_cost',
'margin_rate',
'is_active',
];
foreach ($columns as $column) {
if (Schema::hasColumn('products', $column)) {
$table->dropColumn($column);
}
}
});
}
};

View File

@@ -0,0 +1,104 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* BP-MES (품목 기준관리 중심 MES/ERP) Phase 1
* product_components 테이블 확장: BOMLine 수식 계산 및 조건부 BOM 지원
*
* 추가 필드:
* - 수식 계산: quantity_formula (핵심 기능)
* - 조건부 BOM: condition
* - 절곡품: is_bending, bending_diagram, bending_details (3개)
*
* 총 5개 필드 추가
*
* @see /claudedocs/mes/00_baseline/BACKEND_DEVELOPMENT_ROADMAP_V2.md
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('product_components', function (Blueprint $table) {
// ================================================
// 핵심 필드: 수식 계산 및 조건부 BOM
// ================================================
if (! Schema::hasColumn('product_components', 'quantity_formula')) {
$table->text('quantity_formula')
->nullable()
->after('quantity')
->comment('수량 계산 공식 (예: "W * 2", "H + 100", "G/1000*1.02")');
}
if (! Schema::hasColumn('product_components', 'condition')) {
$table->text('condition')
->nullable()
->after('quantity_formula')
->comment('조건부 BOM 조건식 (예: "MOTOR=\'Y\'", "WIDTH > 3000")');
}
// ================================================
// 절곡품 관련 필드 (3개)
// ================================================
if (! Schema::hasColumn('product_components', 'is_bending')) {
$table->boolean('is_bending')
->default(false)
->after('condition')
->comment('절곡품 여부');
}
if (! Schema::hasColumn('product_components', 'bending_diagram')) {
$table->string('bending_diagram', 255)
->nullable()
->after('is_bending')
->comment('절곡 전개도 이미지 URL');
}
if (! Schema::hasColumn('product_components', 'bending_details')) {
$table->json('bending_details')
->nullable()
->after('bending_diagram')
->comment('절곡 전개도 상세 데이터 (BendingDetail[])');
}
// ================================================
// 인덱스 추가 (조회 최적화)
// ================================================
// is_bending 인덱스
$hasIsBendingIndex = collect(\DB::select('SHOW INDEX FROM `product_components`'))
->contains(fn ($r) => $r->Key_name === 'idx_product_components_is_bending');
if (! $hasIsBendingIndex && Schema::hasColumn('product_components', 'is_bending')) {
$table->index('is_bending', 'idx_product_components_is_bending');
}
});
}
public function down(): void
{
Schema::table('product_components', function (Blueprint $table) {
// 인덱스 제거
$hasIsBendingIndex = collect(\DB::select('SHOW INDEX FROM `product_components`'))
->contains(fn ($r) => $r->Key_name === 'idx_product_components_is_bending');
if ($hasIsBendingIndex) {
$table->dropIndex('idx_product_components_is_bending');
}
// 컬럼 제거 (역순)
$columns = [
'bending_details',
'bending_diagram',
'is_bending',
'condition',
'quantity_formula',
];
foreach ($columns as $column) {
if (Schema::hasColumn('product_components', $column)) {
$table->dropColumn($column);
}
}
});
}
};