feat: Item Master 하이브리드 구조 전환 및 독립 API 추가
- CASCADE FK → 독립 엔티티 + entity_relationships 링크 테이블 - 독립 API 10개 추가 (섹션/필드/BOM CRUD, clone, usage) - SectionTemplate 모델 제거 → ItemSection.is_template 통합 - 페이지-섹션, 섹션-필드, 섹션-BOM 링크/언링크 API 14개 추가 - Swagger 문서 업데이트
This commit is contained in:
@@ -63,7 +63,17 @@ public function up(): void
|
||||
DB::statement("ALTER TABLE categories
|
||||
MODIFY COLUMN is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '활성여부(1=활성,0=비활성)'");
|
||||
|
||||
// A-3. 유니크 (tenant_id, code) → (tenant_id, code_group, code)
|
||||
// A-3. code_group 컬럼 추가 (유니크 인덱스 추가 전에 필요)
|
||||
Schema::table('categories', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('categories', 'code_group')) {
|
||||
$table->string('code_group', 30)
|
||||
->default('category')
|
||||
->after('code')
|
||||
->comment('코드 그룹');
|
||||
}
|
||||
});
|
||||
|
||||
// A-4. 유니크 (tenant_id, code) → (tenant_id, code_group, code)
|
||||
$this->dropUniqueIfColumns('categories', ['tenant_id', 'code']);
|
||||
$idx = collect(DB::select('SHOW INDEX FROM `categories`'))->groupBy('Key_name');
|
||||
$hasTarget = false;
|
||||
@@ -80,7 +90,7 @@ public function up(): void
|
||||
ADD UNIQUE KEY `uq_tenant_codegroup_code` (`tenant_id`,`code_group`,`code`)');
|
||||
}
|
||||
|
||||
// A-4. profile_code 추가 (common_codes.code, code_group='capability_profile')
|
||||
// A-5. profile_code 추가 (common_codes.code, code_group='capability_profile')
|
||||
Schema::table('categories', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('categories', 'profile_code')) {
|
||||
$table->string('profile_code', 30)
|
||||
|
||||
@@ -12,32 +12,39 @@
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('materials', function (Blueprint $table) {
|
||||
// material_type 컬럼 추가 (SM=부자재, RM=원자재, CS=소모품)
|
||||
$table->string('material_type', 10)
|
||||
->nullable()
|
||||
->after('category_id')
|
||||
->comment('자재 타입: SM=부자재, RM=원자재, CS=소모품');
|
||||
// material_type 컬럼이 없을 때만 추가
|
||||
if (! Schema::hasColumn('materials', 'material_type')) {
|
||||
Schema::table('materials', function (Blueprint $table) {
|
||||
// material_type 컬럼 추가 (SM=부자재, RM=원자재, CS=소모품)
|
||||
$table->string('material_type', 10)
|
||||
->nullable()
|
||||
->after('category_id')
|
||||
->comment('자재 타입: SM=부자재, RM=원자재, CS=소모품');
|
||||
});
|
||||
}
|
||||
|
||||
// 조회 성능을 위한 인덱스
|
||||
$table->index('material_type');
|
||||
});
|
||||
// 인덱스가 없을 때만 추가
|
||||
$indexes = collect(DB::select('SHOW INDEX FROM materials'))
|
||||
->pluck('Key_name')
|
||||
->toArray();
|
||||
|
||||
// 기존 데이터 업데이트
|
||||
if (! in_array('materials_material_type_index', $indexes)) {
|
||||
Schema::table('materials', function (Blueprint $table) {
|
||||
$table->index('material_type');
|
||||
});
|
||||
}
|
||||
|
||||
// 기존 데이터 업데이트 (options 컬럼이 없으므로 기본값 'SM' 설정)
|
||||
DB::statement("
|
||||
UPDATE materials
|
||||
SET material_type = CASE
|
||||
WHEN JSON_EXTRACT(options, '$.categories.item_type.code') = 'RAW' THEN 'RM'
|
||||
WHEN JSON_EXTRACT(options, '$.categories.item_type.code') = 'SUB' THEN 'SM'
|
||||
ELSE 'SM'
|
||||
END
|
||||
WHERE tenant_id = 1
|
||||
AND deleted_at IS NULL
|
||||
SET material_type = 'SM'
|
||||
WHERE deleted_at IS NULL
|
||||
AND material_type IS NULL
|
||||
");
|
||||
|
||||
// 모든 자재에 타입이 설정되었으므로 NOT NULL로 변경
|
||||
Schema::table('materials', function (Blueprint $table) {
|
||||
$table->string('material_type', 10)->nullable(false)->change();
|
||||
$table->string('material_type', 10)->default('SM')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Item Master 하이브리드 구조 전환 - Phase 1
|
||||
*
|
||||
* 목적: CASCADE FK 기반 계층 구조를 독립 엔티티 + 링크 테이블 구조로 전환
|
||||
*
|
||||
* 변경 내용:
|
||||
* 1. item_sections에서 page_id FK 제거 (컬럼은 유지, FK 제약만 제거)
|
||||
* 2. item_fields에서 section_id FK 제거
|
||||
* 3. item_bom_items에서 section_id FK 제거
|
||||
* 4. 모든 item 관련 테이블에 group_id 추가 (카테고리 격리용, 기본값 1 = 품목관리)
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// 1. item_sections: page_id FK 제거 (컬럼 유지, FK만 제거)
|
||||
Schema::table('item_sections', function (Blueprint $table) {
|
||||
$table->dropForeign('fk_item_sections_page');
|
||||
});
|
||||
|
||||
// 2. item_fields: section_id FK 제거 (컬럼 유지, FK만 제거)
|
||||
Schema::table('item_fields', function (Blueprint $table) {
|
||||
$table->dropForeign('fk_item_fields_section');
|
||||
});
|
||||
|
||||
// 3. item_bom_items: section_id FK 제거 (컬럼 유지, FK만 제거)
|
||||
Schema::table('item_bom_items', function (Blueprint $table) {
|
||||
$table->dropForeign('fk_item_bom_items_section');
|
||||
});
|
||||
|
||||
// 4. 모든 테이블에 group_id 추가 (기본값 1 = 품목관리)
|
||||
Schema::table('item_pages', function (Blueprint $table) {
|
||||
$table->unsignedInteger('group_id')->default(1)->after('tenant_id')->comment('그룹 ID (1: 품목관리)');
|
||||
$table->index(['tenant_id', 'group_id'], 'idx_item_pages_tenant_group');
|
||||
});
|
||||
|
||||
Schema::table('item_sections', function (Blueprint $table) {
|
||||
$table->unsignedInteger('group_id')->default(1)->after('tenant_id')->comment('그룹 ID (1: 품목관리)');
|
||||
$table->index(['tenant_id', 'group_id'], 'idx_item_sections_tenant_group');
|
||||
});
|
||||
|
||||
Schema::table('item_fields', function (Blueprint $table) {
|
||||
$table->unsignedInteger('group_id')->default(1)->after('tenant_id')->comment('그룹 ID (1: 품목관리)');
|
||||
$table->index(['tenant_id', 'group_id'], 'idx_item_fields_tenant_group');
|
||||
});
|
||||
|
||||
Schema::table('item_bom_items', function (Blueprint $table) {
|
||||
$table->unsignedInteger('group_id')->default(1)->after('tenant_id')->comment('그룹 ID (1: 품목관리)');
|
||||
$table->index(['tenant_id', 'group_id'], 'idx_item_bom_items_tenant_group');
|
||||
});
|
||||
|
||||
Schema::table('section_templates', function (Blueprint $table) {
|
||||
$table->unsignedInteger('group_id')->default(1)->after('tenant_id')->comment('그룹 ID (1: 품목관리)');
|
||||
$table->index(['tenant_id', 'group_id'], 'idx_section_templates_tenant_group');
|
||||
});
|
||||
|
||||
Schema::table('item_master_fields', function (Blueprint $table) {
|
||||
$table->unsignedInteger('group_id')->default(1)->after('tenant_id')->comment('그룹 ID (1: 품목관리)');
|
||||
$table->index(['tenant_id', 'group_id'], 'idx_item_master_fields_tenant_group');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// group_id 인덱스 및 컬럼 제거
|
||||
Schema::table('item_master_fields', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_item_master_fields_tenant_group');
|
||||
$table->dropColumn('group_id');
|
||||
});
|
||||
|
||||
Schema::table('section_templates', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_section_templates_tenant_group');
|
||||
$table->dropColumn('group_id');
|
||||
});
|
||||
|
||||
Schema::table('item_bom_items', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_item_bom_items_tenant_group');
|
||||
$table->dropColumn('group_id');
|
||||
});
|
||||
|
||||
Schema::table('item_fields', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_item_fields_tenant_group');
|
||||
$table->dropColumn('group_id');
|
||||
});
|
||||
|
||||
Schema::table('item_sections', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_item_sections_tenant_group');
|
||||
$table->dropColumn('group_id');
|
||||
});
|
||||
|
||||
Schema::table('item_pages', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_item_pages_tenant_group');
|
||||
$table->dropColumn('group_id');
|
||||
});
|
||||
|
||||
// FK 복원
|
||||
Schema::table('item_bom_items', function (Blueprint $table) {
|
||||
$table->foreign('section_id', 'fk_item_bom_items_section')
|
||||
->references('id')->on('item_sections')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
|
||||
Schema::table('item_fields', function (Blueprint $table) {
|
||||
$table->foreign('section_id', 'fk_item_fields_section')
|
||||
->references('id')->on('item_sections')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
|
||||
Schema::table('item_sections', function (Blueprint $table) {
|
||||
$table->foreign('page_id', 'fk_item_sections_page')
|
||||
->references('id')->on('item_pages')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Item Master 하이브리드 구조 전환 - Phase 2
|
||||
*
|
||||
* 목적: 엔티티 간 관계를 관리하는 범용 링크 테이블 생성
|
||||
*
|
||||
* 지원하는 관계 유형:
|
||||
* - page-section: 페이지와 섹션 연결
|
||||
* - page-field: 페이지와 필드 직접 연결
|
||||
* - section-field: 섹션과 필드 연결
|
||||
* - section-bom: 섹션과 BOM 항목 연결
|
||||
*
|
||||
* 특징:
|
||||
* - 다대다(Many-to-Many) 관계 지원
|
||||
* - 동일 엔티티를 여러 부모에 연결 가능
|
||||
* - 부모 삭제 시 링크만 제거, 자식 엔티티는 유지
|
||||
* - group_id로 카테고리 격리
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('entity_relationships', function (Blueprint $table) {
|
||||
$table->id()->comment('관계 ID');
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
$table->unsignedInteger('group_id')->default(1)->comment('그룹 ID (1: 품목관리)');
|
||||
|
||||
// 부모 엔티티 정보
|
||||
$table->string('parent_type', 50)->comment('부모 엔티티 타입 (page, section)');
|
||||
$table->unsignedBigInteger('parent_id')->comment('부모 엔티티 ID');
|
||||
|
||||
// 자식 엔티티 정보
|
||||
$table->string('child_type', 50)->comment('자식 엔티티 타입 (section, field, bom)');
|
||||
$table->unsignedBigInteger('child_id')->comment('자식 엔티티 ID');
|
||||
|
||||
// 관계 메타데이터
|
||||
$table->integer('order_no')->default(0)->comment('정렬 순서');
|
||||
$table->json('metadata')->nullable()->comment('관계 메타데이터 (추가 설정)');
|
||||
|
||||
// 감사 컬럼
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID');
|
||||
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID');
|
||||
$table->timestamps();
|
||||
|
||||
// 인덱스
|
||||
$table->index(['tenant_id', 'group_id'], 'idx_entity_rel_tenant_group');
|
||||
$table->index(['parent_type', 'parent_id'], 'idx_entity_rel_parent');
|
||||
$table->index(['child_type', 'child_id'], 'idx_entity_rel_child');
|
||||
$table->index(['parent_type', 'parent_id', 'order_no'], 'idx_entity_rel_parent_order');
|
||||
|
||||
// 유니크 제약 (동일 부모-자식 관계 중복 방지)
|
||||
$table->unique(
|
||||
['tenant_id', 'group_id', 'parent_type', 'parent_id', 'child_type', 'child_id'],
|
||||
'uq_entity_rel_parent_child'
|
||||
);
|
||||
|
||||
// 외래키
|
||||
$table->foreign('tenant_id', 'fk_entity_rel_tenant')
|
||||
->references('id')->on('tenants')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('entity_relationships');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Item Master 하이브리드 구조 전환 - Phase 3
|
||||
*
|
||||
* 목적: 기존 FK 기반 관계 데이터를 entity_relationships 테이블로 이관
|
||||
*
|
||||
* 이관 대상:
|
||||
* 1. item_sections.page_id → entity_relationships (page-section)
|
||||
* 2. item_fields.section_id → entity_relationships (section-field)
|
||||
* 3. item_bom_items.section_id → entity_relationships (section-bom)
|
||||
*
|
||||
* 주의사항:
|
||||
* - 기존 컬럼(page_id, section_id)의 값은 유지 (하위 호환성)
|
||||
* - soft delete된 레코드도 이관 대상에 포함
|
||||
* - order_no 값을 그대로 복사
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// 1. item_sections → entity_relationships (page-section)
|
||||
DB::statement("
|
||||
INSERT INTO entity_relationships (tenant_id, group_id, parent_type, parent_id, child_type, child_id, order_no, created_by, created_at, updated_at)
|
||||
SELECT
|
||||
s.tenant_id,
|
||||
COALESCE(s.group_id, 1) as group_id,
|
||||
'page' as parent_type,
|
||||
s.page_id as parent_id,
|
||||
'section' as child_type,
|
||||
s.id as child_id,
|
||||
s.order_no,
|
||||
s.created_by,
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM item_sections s
|
||||
WHERE s.page_id IS NOT NULL
|
||||
ON DUPLICATE KEY UPDATE updated_at = NOW()
|
||||
");
|
||||
|
||||
// 2. item_fields → entity_relationships (section-field)
|
||||
DB::statement("
|
||||
INSERT INTO entity_relationships (tenant_id, group_id, parent_type, parent_id, child_type, child_id, order_no, created_by, created_at, updated_at)
|
||||
SELECT
|
||||
f.tenant_id,
|
||||
COALESCE(f.group_id, 1) as group_id,
|
||||
'section' as parent_type,
|
||||
f.section_id as parent_id,
|
||||
'field' as child_type,
|
||||
f.id as child_id,
|
||||
f.order_no,
|
||||
f.created_by,
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM item_fields f
|
||||
WHERE f.section_id IS NOT NULL
|
||||
ON DUPLICATE KEY UPDATE updated_at = NOW()
|
||||
");
|
||||
|
||||
// 3. item_bom_items → entity_relationships (section-bom)
|
||||
// BOM 항목은 order_no가 없으므로 id 기준으로 순서 부여
|
||||
DB::statement("
|
||||
INSERT INTO entity_relationships (tenant_id, group_id, parent_type, parent_id, child_type, child_id, order_no, created_by, created_at, updated_at)
|
||||
SELECT
|
||||
b.tenant_id,
|
||||
COALESCE(b.group_id, 1) as group_id,
|
||||
'section' as parent_type,
|
||||
b.section_id as parent_id,
|
||||
'bom' as child_type,
|
||||
b.id as child_id,
|
||||
@rownum := @rownum + 1 as order_no,
|
||||
b.created_by,
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM item_bom_items b, (SELECT @rownum := 0) r
|
||||
WHERE b.section_id IS NOT NULL
|
||||
ORDER BY b.section_id, b.id
|
||||
ON DUPLICATE KEY UPDATE updated_at = NOW()
|
||||
");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// 이관된 데이터 삭제 (page-section, section-field, section-bom 관계만)
|
||||
DB::table('entity_relationships')
|
||||
->whereIn('parent_type', ['page', 'section'])
|
||||
->whereIn('child_type', ['section', 'field', 'bom'])
|
||||
->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* 1. item_sections에 is_template, description 컬럼 추가
|
||||
* 2. section_templates 데이터를 item_sections로 이관
|
||||
* 3. section_templates 테이블 제거
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// 1. item_sections에 is_template, description 컬럼 추가
|
||||
Schema::table('item_sections', function (Blueprint $table) {
|
||||
$table->boolean('is_template')->default(false)->after('order_no')->comment('템플릿 여부');
|
||||
$table->boolean('is_default')->default(false)->after('is_template')->comment('기본 템플릿 여부');
|
||||
$table->text('description')->nullable()->after('is_default')->comment('설명');
|
||||
});
|
||||
|
||||
// 2. section_templates 데이터를 item_sections로 이관
|
||||
if (Schema::hasTable('section_templates')) {
|
||||
$templates = DB::table('section_templates')->whereNull('deleted_at')->get();
|
||||
|
||||
foreach ($templates as $template) {
|
||||
DB::table('item_sections')->insert([
|
||||
'tenant_id' => $template->tenant_id,
|
||||
'group_id' => $template->group_id ?? 1,
|
||||
'title' => $template->title,
|
||||
'type' => $template->type,
|
||||
'order_no' => 0,
|
||||
'is_template' => true,
|
||||
'is_default' => $template->is_default,
|
||||
'description' => $template->description,
|
||||
'created_by' => $template->created_by,
|
||||
'updated_by' => $template->updated_by,
|
||||
'created_at' => $template->created_at,
|
||||
'updated_at' => $template->updated_at,
|
||||
]);
|
||||
}
|
||||
|
||||
// 3. section_templates 테이블 제거
|
||||
Schema::dropIfExists('section_templates');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// 1. section_templates 테이블 복원
|
||||
Schema::create('section_templates', function (Blueprint $table) {
|
||||
$table->id()->comment('섹션 템플릿 ID');
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
$table->unsignedInteger('group_id')->default(1)->comment('그룹 ID');
|
||||
$table->string('title')->comment('템플릿명');
|
||||
$table->enum('type', ['fields', 'bom'])->default('fields')->comment('섹션 타입');
|
||||
$table->text('description')->nullable()->comment('설명');
|
||||
$table->boolean('is_default')->default(false)->comment('기본 템플릿 여부');
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID');
|
||||
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID');
|
||||
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID');
|
||||
$table->timestamps();
|
||||
$table->softDeletes()->comment('소프트 삭제');
|
||||
|
||||
$table->index('tenant_id', 'idx_section_templates_tenant_id');
|
||||
$table->foreign('tenant_id', 'fk_section_templates_tenant')
|
||||
->references('id')->on('tenants')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
|
||||
// 2. item_sections의 is_template=true 데이터를 section_templates로 이관
|
||||
$templates = DB::table('item_sections')
|
||||
->where('is_template', true)
|
||||
->whereNull('deleted_at')
|
||||
->get();
|
||||
|
||||
foreach ($templates as $template) {
|
||||
DB::table('section_templates')->insert([
|
||||
'tenant_id' => $template->tenant_id,
|
||||
'group_id' => $template->group_id,
|
||||
'title' => $template->title,
|
||||
'type' => $template->type,
|
||||
'description' => $template->description,
|
||||
'is_default' => $template->is_default,
|
||||
'created_by' => $template->created_by,
|
||||
'updated_by' => $template->updated_by,
|
||||
'created_at' => $template->created_at,
|
||||
'updated_at' => $template->updated_at,
|
||||
]);
|
||||
}
|
||||
|
||||
// 3. item_sections에서 is_template=true 데이터 삭제
|
||||
DB::table('item_sections')->where('is_template', true)->delete();
|
||||
|
||||
// 4. item_sections에서 컬럼 제거
|
||||
Schema::table('item_sections', function (Blueprint $table) {
|
||||
$table->dropColumn(['is_template', 'is_default', 'description']);
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user