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:
@@ -1,6 +1,6 @@
|
||||
# 논리적 데이터베이스 관계 문서
|
||||
|
||||
> **자동 생성**: 2025-11-11 11:24:13
|
||||
> **자동 생성**: 2025-11-14 08:41:17
|
||||
> **소스**: Eloquent 모델 관계 분석
|
||||
|
||||
## 📊 모델별 관계 현황
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user