refactor: BP-MES Phase 1 하이브리드 구조 전환
- products 테이블: 6개 고정 필드 + attributes JSON - product_components 테이블: 수식/조건 + attributes JSON - tenant_stat_fields 테이블: 테넌트별 통계 필드 설정 - stat_snapshots 테이블: 통계 캐싱 - BP-MES CategoryFields Seeder: 제품/부품/절곡품 카테고리 필드 - BP-MES TenantStatFields Seeder: 통계 필드 설정 [변경 사항] 삭제: - 2025_11_13_120000_extend_products_table_for_bp_mes.php - 2025_11_13_120001_extend_product_components_table_for_bp_mes.php 추가: - 2025_11_14_000001_add_hybrid_fields_to_products_table.php - 2025_11_14_000002_add_attributes_to_product_components_table.php - 2025_11_14_000003_create_tenant_stat_fields_table.php - 2025_11_14_000004_create_stat_snapshots_table.php - BpMesCategoryFieldsSeeder.php - BpMesTenantStatFieldsSeeder.php [배경] 멀티테넌트 시스템의 유연성 확보를 위해 고정 필드를 최소화하고 동적 필드 시스템(category_fields + attributes JSON)으로 전환. 통계 성능을 위해 자주 조회하는 분류 필드(product_category, part_type)는 고정 컬럼으로 유지하고 인덱싱.
This commit is contained in:
232
database/seeders/BpMesCategoryFieldsSeeder.php
Normal file
232
database/seeders/BpMesCategoryFieldsSeeder.php
Normal file
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* BP-MES 품목 관리 시스템 카테고리 필드 시더
|
||||
*
|
||||
* 목적: BP-MES 테넌트를 위한 제품/부품/절곡품 카테고리 및 동적 필드 생성
|
||||
*
|
||||
* 구조:
|
||||
* - bp_mes_products (루트)
|
||||
* - fg_products (완제품/FG) - 마진율, 비용, LOT 관리
|
||||
* - pt_products (부품/PT) - 용도, 설치방식, 조립 정보
|
||||
* - bending_products (절곡품) - 절곡 상세, 재질, 길이
|
||||
*
|
||||
* 실행: php artisan db:seed --class=BpMesCategoryFieldsSeeder
|
||||
*
|
||||
* @see /claudedocs/mes/ITEM_MANAGEMENT_MIGRATION_GUIDE.md
|
||||
*/
|
||||
class BpMesCategoryFieldsSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* BP-MES 테넌트 ID (실제 환경에 맞게 수정 필요)
|
||||
*/
|
||||
private const TENANT_ID = 1;
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
// 1. BP-MES 루트 카테고리 생성
|
||||
$rootCategoryId = DB::table('categories')->insertGetId([
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'parent_id' => null,
|
||||
'code_group' => 'bp_mes',
|
||||
'code' => 'bp_mes_products',
|
||||
'name' => 'BP-MES 품목 관리',
|
||||
'description' => 'BP-MES 품목 기준관리 시스템 루트 카테고리',
|
||||
'sort_order' => 1,
|
||||
'profile_code' => 'bp_mes_root',
|
||||
'is_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
// 2. 완제품(FG) 카테고리 생성
|
||||
$fgCategoryId = DB::table('categories')->insertGetId([
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'parent_id' => $rootCategoryId,
|
||||
'code_group' => 'bp_mes',
|
||||
'code' => 'fg_products',
|
||||
'name' => '완제품(FG)',
|
||||
'description' => '판매 가능한 완제품 카테고리 (마진율, 비용 관리)',
|
||||
'sort_order' => 1,
|
||||
'profile_code' => 'fg_category',
|
||||
'is_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
// 3. 부품(PT) 카테고리 생성
|
||||
$ptCategoryId = DB::table('categories')->insertGetId([
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'parent_id' => $rootCategoryId,
|
||||
'code_group' => 'bp_mes',
|
||||
'code' => 'pt_products',
|
||||
'name' => '부품(PT)',
|
||||
'description' => '조립용 부품 카테고리 (용도, 설치방식, 규격)',
|
||||
'sort_order' => 2,
|
||||
'profile_code' => 'pt_category',
|
||||
'is_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
// 4. 절곡품 카테고리 생성
|
||||
$bendingCategoryId = DB::table('categories')->insertGetId([
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'parent_id' => $rootCategoryId,
|
||||
'code_group' => 'bp_mes',
|
||||
'code' => 'bending_products',
|
||||
'name' => '절곡품',
|
||||
'description' => '금속 절곡 가공품 카테고리 (절곡도, 재질, 길이)',
|
||||
'sort_order' => 3,
|
||||
'profile_code' => 'bending_category',
|
||||
'is_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
// 5. 완제품(FG) 동적 필드 정의
|
||||
$fgFields = [
|
||||
// 비용 관리 (통계 필요)
|
||||
['key' => 'margin_rate', 'name' => '마진율(%)', 'type' => 'decimal', 'required' => false, 'order' => 10,
|
||||
'desc' => '제품 마진율 (통계 대상)'],
|
||||
|
||||
['key' => 'processing_cost', 'name' => '가공비(원)', 'type' => 'decimal', 'required' => false, 'order' => 11,
|
||||
'desc' => '가공 비용 (통계 대상)'],
|
||||
|
||||
['key' => 'labor_cost', 'name' => '인건비(원)', 'type' => 'decimal', 'required' => false, 'order' => 12,
|
||||
'desc' => '인건비 (통계 대상)'],
|
||||
|
||||
['key' => 'install_cost', 'name' => '설치비(원)', 'type' => 'decimal', 'required' => false, 'order' => 13,
|
||||
'desc' => '설치 비용 (통계 대상)'],
|
||||
|
||||
// LOT 관리
|
||||
['key' => 'lot_abbreviation', 'name' => 'LOT 약어', 'type' => 'text', 'required' => false, 'order' => 20,
|
||||
'desc' => 'LOT 번호 생성용 약어'],
|
||||
|
||||
['key' => 'note', 'name' => '비고', 'type' => 'textarea', 'required' => false, 'order' => 21,
|
||||
'desc' => '제품 관련 메모 및 특이사항'],
|
||||
|
||||
// 인증 정보
|
||||
['key' => 'certification_number', 'name' => '인증번호', 'type' => 'text', 'required' => false, 'order' => 30,
|
||||
'desc' => '제품 인증번호'],
|
||||
|
||||
['key' => 'certification_start_date', 'name' => '인증시작일', 'type' => 'date', 'required' => false, 'order' => 31,
|
||||
'desc' => '인증 유효기간 시작일'],
|
||||
|
||||
['key' => 'certification_end_date', 'name' => '인증종료일', 'type' => 'date', 'required' => false, 'order' => 32,
|
||||
'desc' => '인증 유효기간 종료일'],
|
||||
|
||||
// 파일 관리
|
||||
['key' => 'specification_file', 'name' => '규격파일', 'type' => 'file', 'required' => false, 'order' => 40,
|
||||
'desc' => '제품 규격서 파일 경로'],
|
||||
|
||||
['key' => 'specification_file_name', 'name' => '규격파일명', 'type' => 'text', 'required' => false, 'order' => 41,
|
||||
'desc' => '규격서 파일 원본명'],
|
||||
|
||||
['key' => 'certification_file', 'name' => '인증파일', 'type' => 'file', 'required' => false, 'order' => 42,
|
||||
'desc' => '인증서 파일 경로'],
|
||||
|
||||
['key' => 'certification_file_name', 'name' => '인증파일명', 'type' => 'text', 'required' => false, 'order' => 43,
|
||||
'desc' => '인증서 파일 원본명'],
|
||||
|
||||
// 확장 옵션
|
||||
['key' => 'options', 'name' => '옵션정보', 'type' => 'json', 'required' => false, 'order' => 50,
|
||||
'desc' => '추가 옵션 정보 (JSON 형태)'],
|
||||
];
|
||||
|
||||
// 6. 부품(PT) 동적 필드 정의
|
||||
$ptFields = [
|
||||
// 부품 분류 및 용도
|
||||
['key' => 'part_usage', 'name' => '용도', 'type' => 'text', 'required' => false, 'order' => 10,
|
||||
'desc' => '부품 사용 용도'],
|
||||
|
||||
['key' => 'installation_type', 'name' => '설치방식', 'type' => 'select', 'required' => false, 'order' => 11,
|
||||
'options' => ['CEILING', 'WALL', 'FLOOR', 'HANGING'],
|
||||
'desc' => '설치 방식 (천장형, 벽면형, 바닥형, 현가형)'],
|
||||
|
||||
['key' => 'assembly_type', 'name' => '조립타입', 'type' => 'select', 'required' => false, 'order' => 12,
|
||||
'options' => ['WELDING', 'BOLT', 'RIVET', 'ADHESIVE'],
|
||||
'desc' => '조립 방식 (용접, 볼트, 리벳, 접착)'],
|
||||
|
||||
// 치수 정보
|
||||
['key' => 'side_spec_width', 'name' => '측면규격_폭(mm)', 'type' => 'decimal', 'required' => false, 'order' => 20,
|
||||
'desc' => '측면 규격 폭'],
|
||||
|
||||
['key' => 'side_spec_height', 'name' => '측면규격_높이(mm)', 'type' => 'decimal', 'required' => false, 'order' => 21,
|
||||
'desc' => '측면 규격 높이'],
|
||||
|
||||
['key' => 'assembly_length', 'name' => '조립길이(mm)', 'type' => 'decimal', 'required' => false, 'order' => 22,
|
||||
'desc' => '조립 시 길이'],
|
||||
|
||||
// 가이드레일 정보
|
||||
['key' => 'guide_rail_model_type', 'name' => '가이드레일모델타입', 'type' => 'select', 'required' => false, 'order' => 30,
|
||||
'options' => ['TYPE_A', 'TYPE_B', 'TYPE_C', 'CUSTOM'],
|
||||
'desc' => '가이드레일 모델 타입'],
|
||||
|
||||
['key' => 'guide_rail_model', 'name' => '가이드레일모델', 'type' => 'text', 'required' => false, 'order' => 31,
|
||||
'desc' => '가이드레일 모델명'],
|
||||
];
|
||||
|
||||
// 7. 절곡품 동적 필드 정의
|
||||
$bendingFields = [
|
||||
// 절곡 정보
|
||||
['key' => 'bending_diagram', 'name' => '절곡도', 'type' => 'file', 'required' => false, 'order' => 10,
|
||||
'desc' => '절곡 도면 파일 경로'],
|
||||
|
||||
['key' => 'bending_details', 'name' => '절곡상세', 'type' => 'json', 'required' => false, 'order' => 11,
|
||||
'desc' => '절곡 상세 정보 (각도, 위치 등 JSON)'],
|
||||
|
||||
// 재질 및 치수
|
||||
['key' => 'material', 'name' => '재질', 'type' => 'select', 'required' => true, 'order' => 20,
|
||||
'options' => ['STEEL', 'STAINLESS', 'ALUMINUM', 'GALVANIZED'],
|
||||
'desc' => '금속 재질 (강판, 스텐, 알루미늄, 아연도금)'],
|
||||
|
||||
['key' => 'length', 'name' => '길이(mm)', 'type' => 'decimal', 'required' => true, 'order' => 21,
|
||||
'desc' => '절곡품 길이'],
|
||||
|
||||
['key' => 'bending_length', 'name' => '절곡길이(mm)', 'type' => 'decimal', 'required' => false, 'order' => 22,
|
||||
'desc' => '절곡 부분 길이'],
|
||||
];
|
||||
|
||||
// 8. 완제품(FG) 카테고리 필드 생성
|
||||
foreach ($fgFields as $field) {
|
||||
$this->insertCategoryField($fgCategoryId, $field);
|
||||
}
|
||||
|
||||
// 9. 부품(PT) 카테고리 필드 생성
|
||||
foreach ($ptFields as $field) {
|
||||
$this->insertCategoryField($ptCategoryId, $field);
|
||||
}
|
||||
|
||||
// 10. 절곡품 카테고리 필드 생성
|
||||
foreach ($bendingFields as $field) {
|
||||
$this->insertCategoryField($bendingCategoryId, $field);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* category_fields 테이블에 필드 삽입
|
||||
*/
|
||||
private function insertCategoryField(int $categoryId, array $field): void
|
||||
{
|
||||
DB::table('category_fields')->insert([
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'category_id' => $categoryId,
|
||||
'field_key' => $field['key'],
|
||||
'field_name' => $field['name'],
|
||||
'field_type' => $field['type'],
|
||||
'is_required' => $field['required'],
|
||||
'sort_order' => $field['order'],
|
||||
'default_value' => $field['default'] ?? null,
|
||||
'options' => isset($field['options']) ? json_encode($field['options']) : null,
|
||||
'description' => $field['desc'],
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
174
database/seeders/BpMesTenantStatFieldsSeeder.php
Normal file
174
database/seeders/BpMesTenantStatFieldsSeeder.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* BP-MES 테넌트 통계 필드 설정 시더
|
||||
*
|
||||
* 목적: BP-MES 테넌트가 통계를 내고 싶은 필드를 tenant_stat_fields 테이블에 선언
|
||||
*
|
||||
* 효과:
|
||||
* - 일일 스케줄러가 이 필드들의 통계를 계산하여 stat_snapshots에 저장
|
||||
* - 대시보드 및 통계 화면에서 빠른 조회 가능
|
||||
* - 잠재적으로 MySQL Virtual Column 최적화 대상 식별
|
||||
*
|
||||
* 실행: php artisan db:seed --class=BpMesTenantStatFieldsSeeder
|
||||
*
|
||||
* @see /claudedocs/mes/ITEM_MANAGEMENT_MIGRATION_GUIDE.md
|
||||
*/
|
||||
class BpMesTenantStatFieldsSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* BP-MES 테넌트 ID (실제 환경에 맞게 수정 필요)
|
||||
*/
|
||||
private const TENANT_ID = 1;
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
// 1. products 테이블의 통계 필드 정의
|
||||
$productStatFields = [
|
||||
// 마진율 통계 (중요 지표)
|
||||
[
|
||||
'target_table' => 'products',
|
||||
'field_key' => 'margin_rate',
|
||||
'field_name' => '마진율(%)',
|
||||
'field_type' => 'decimal',
|
||||
'aggregation_types' => ['avg', 'min', 'max'],
|
||||
'is_critical' => true,
|
||||
'display_order' => 1,
|
||||
'description' => '제품별 마진율 통계 (평균, 최소, 최대)',
|
||||
],
|
||||
|
||||
// 가공비 통계
|
||||
[
|
||||
'target_table' => 'products',
|
||||
'field_key' => 'processing_cost',
|
||||
'field_name' => '가공비(원)',
|
||||
'field_type' => 'decimal',
|
||||
'aggregation_types' => ['avg', 'sum', 'min', 'max'],
|
||||
'is_critical' => true,
|
||||
'display_order' => 2,
|
||||
'description' => '가공비 통계 (평균, 합계, 최소, 최대)',
|
||||
],
|
||||
|
||||
// 인건비 통계
|
||||
[
|
||||
'target_table' => 'products',
|
||||
'field_key' => 'labor_cost',
|
||||
'field_name' => '인건비(원)',
|
||||
'field_type' => 'decimal',
|
||||
'aggregation_types' => ['avg', 'sum', 'min', 'max'],
|
||||
'is_critical' => true,
|
||||
'display_order' => 3,
|
||||
'description' => '인건비 통계 (평균, 합계, 최소, 최대)',
|
||||
],
|
||||
|
||||
// 설치비 통계
|
||||
[
|
||||
'target_table' => 'products',
|
||||
'field_key' => 'install_cost',
|
||||
'field_name' => '설치비(원)',
|
||||
'field_type' => 'decimal',
|
||||
'aggregation_types' => ['avg', 'sum', 'min', 'max'],
|
||||
'is_critical' => true,
|
||||
'display_order' => 4,
|
||||
'description' => '설치비 통계 (평균, 합계, 최소, 최대)',
|
||||
],
|
||||
|
||||
// 절곡 길이 통계 (선택적)
|
||||
[
|
||||
'target_table' => 'products',
|
||||
'field_key' => 'bending_length',
|
||||
'field_name' => '절곡길이(mm)',
|
||||
'field_type' => 'decimal',
|
||||
'aggregation_types' => ['avg', 'min', 'max'],
|
||||
'is_critical' => false,
|
||||
'display_order' => 10,
|
||||
'description' => '절곡품 길이 통계 (선택적 집계)',
|
||||
],
|
||||
|
||||
// 조립 길이 통계 (선택적)
|
||||
[
|
||||
'target_table' => 'products',
|
||||
'field_key' => 'assembly_length',
|
||||
'field_name' => '조립길이(mm)',
|
||||
'field_type' => 'decimal',
|
||||
'aggregation_types' => ['avg', 'min', 'max'],
|
||||
'is_critical' => false,
|
||||
'display_order' => 11,
|
||||
'description' => '부품 조립 길이 통계 (선택적 집계)',
|
||||
],
|
||||
|
||||
// 측면 규격 통계 (선택적)
|
||||
[
|
||||
'target_table' => 'products',
|
||||
'field_key' => 'side_spec_width',
|
||||
'field_name' => '측면규격_폭(mm)',
|
||||
'field_type' => 'decimal',
|
||||
'aggregation_types' => ['avg', 'min', 'max'],
|
||||
'is_critical' => false,
|
||||
'display_order' => 12,
|
||||
'description' => '측면 규격 폭 통계 (선택적 집계)',
|
||||
],
|
||||
|
||||
[
|
||||
'target_table' => 'products',
|
||||
'field_key' => 'side_spec_height',
|
||||
'field_name' => '측면규격_높이(mm)',
|
||||
'field_type' => 'decimal',
|
||||
'aggregation_types' => ['avg', 'min', 'max'],
|
||||
'is_critical' => false,
|
||||
'display_order' => 13,
|
||||
'description' => '측면 규격 높이 통계 (선택적 집계)',
|
||||
],
|
||||
];
|
||||
|
||||
// 2. materials 테이블의 통계 필드 정의 (향후 확장용 예시)
|
||||
$materialStatFields = [
|
||||
// 예시: 자재 단가 통계
|
||||
[
|
||||
'target_table' => 'materials',
|
||||
'field_key' => 'unit_price',
|
||||
'field_name' => '단가(원)',
|
||||
'field_type' => 'decimal',
|
||||
'aggregation_types' => ['avg', 'min', 'max'],
|
||||
'is_critical' => false,
|
||||
'display_order' => 1,
|
||||
'description' => '자재 단가 통계 (향후 활용)',
|
||||
],
|
||||
];
|
||||
|
||||
// 3. products 통계 필드 생성
|
||||
foreach ($productStatFields as $field) {
|
||||
$this->insertTenantStatField($field);
|
||||
}
|
||||
|
||||
// 4. materials 통계 필드 생성 (향후 확장)
|
||||
foreach ($materialStatFields as $field) {
|
||||
$this->insertTenantStatField($field);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* tenant_stat_fields 테이블에 필드 삽입
|
||||
*/
|
||||
private function insertTenantStatField(array $field): void
|
||||
{
|
||||
DB::table('tenant_stat_fields')->insert([
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'target_table' => $field['target_table'],
|
||||
'field_key' => $field['field_key'],
|
||||
'field_name' => $field['field_name'],
|
||||
'field_type' => $field['field_type'],
|
||||
'aggregation_types' => json_encode($field['aggregation_types']),
|
||||
'is_critical' => $field['is_critical'],
|
||||
'display_order' => $field['display_order'],
|
||||
'description' => $field['description'],
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user