feat: ItemMaster 데이터베이스 구조 구축 (9개 테이블)

- 마이그레이션 9개 생성 (unit_options, section_templates, item_master_fields, item_pages, item_sections, item_fields, item_bom_items, custom_tabs, tab_columns)
- Eloquent 모델 9개 구현 (ItemMaster 네임스페이스)
- ItemMasterSeeder 작성 및 테스트 데이터 생성

주요 특징:
- Multi-tenant 지원 (BelongsToTenant trait)
- Soft Delete 적용 (deleted_at, deleted_by)
- 감사 로그 지원 (created_by, updated_by)
- JSON 필드로 동적 속성 지원 (display_condition, validation_rules, options, properties)
- FK 제약조건 및 Composite Index 설정
- 계층 구조 (ItemPage → ItemSection → ItemField/ItemBomItem)

SAM API Development Rules 준수
This commit is contained in:
2025-11-20 16:36:55 +09:00
parent 8ce8a35f30
commit 7109fc5199
21 changed files with 1370 additions and 1 deletions

View File

@@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('unit_options', function (Blueprint $table) {
$table->id()->comment('단위 옵션 ID');
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->string('label', 100)->comment('단위 라벨');
$table->string('value', 50)->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_unit_options_tenant_id');
// 외래키
$table->foreign('tenant_id', 'fk_unit_options_tenant')
->references('id')->on('tenants')
->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('unit_options');
}
};

View File

@@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('section_templates', function (Blueprint $table) {
$table->id()->comment('섹션 템플릿 ID');
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->string('title')->comment('템플릿명');
$table->enum('type', ['fields', 'bom'])->default('fields')->comment('섹션 타입 (fields: 필드형, bom: BOM형)');
$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');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('section_templates');
}
};

View File

@@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('item_master_fields', function (Blueprint $table) {
$table->id()->comment('마스터 필드 ID');
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->string('field_name')->comment('필드명');
$table->enum('field_type', ['textbox', 'number', 'dropdown', 'checkbox', 'date', 'textarea'])->comment('필드 타입');
$table->string('category', 100)->nullable()->comment('카테고리');
$table->text('description')->nullable()->comment('설명');
$table->boolean('is_common')->default(false)->comment('공통 필드 여부');
$table->text('default_value')->nullable()->comment('기본값');
$table->json('options')->nullable()->comment('드롭다운 옵션 [{"label": "옵션1", "value": "val1"}]');
$table->json('validation_rules')->nullable()->comment('검증 규칙 {"min": 0, "max": 100, "pattern": "regex"}');
$table->json('properties')->nullable()->comment('필드 속성 {"unit": "mm", "precision": 2, "format": "YYYY-MM-DD"}');
$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_item_master_fields_tenant_id');
$table->index('category', 'idx_item_master_fields_category');
// 외래키
$table->foreign('tenant_id', 'fk_item_master_fields_tenant')
->references('id')->on('tenants')
->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('item_master_fields');
}
};

View File

@@ -0,0 +1,45 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('item_pages', function (Blueprint $table) {
$table->id()->comment('품목 페이지 ID');
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->string('page_name')->comment('페이지명');
$table->enum('item_type', ['FG', 'PT', 'SM', 'RM', 'CS'])->comment('품목 유형 (FG: 완제품, PT: 반제품, SM: 부자재, RM: 원자재, CS: 소모품)');
$table->string('absolute_path', 500)->nullable()->comment('절대 경로');
$table->boolean('is_active')->default(true)->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_item_pages_tenant_id');
$table->index('item_type', 'idx_item_pages_item_type');
// 외래키
$table->foreign('tenant_id', 'fk_item_pages_tenant')
->references('id')->on('tenants')
->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('item_pages');
}
};

View File

@@ -0,0 +1,48 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('item_sections', function (Blueprint $table) {
$table->id()->comment('섹션 인스턴스 ID');
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('page_id')->comment('페이지 ID');
$table->string('title')->comment('섹션 제목');
$table->enum('type', ['fields', 'bom'])->default('fields')->comment('섹션 타입 (fields: 필드형, bom: BOM형)');
$table->integer('order_no')->default(0)->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', 'page_id'], 'idx_item_sections_tenant_page');
$table->index(['page_id', 'order_no'], 'idx_item_sections_order');
// 외래키
$table->foreign('tenant_id', 'fk_item_sections_tenant')
->references('id')->on('tenants')
->onDelete('cascade');
$table->foreign('page_id', 'fk_item_sections_page')
->references('id')->on('item_pages')
->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('item_sections');
}
};

View File

@@ -0,0 +1,55 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('item_fields', function (Blueprint $table) {
$table->id()->comment('필드 ID');
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('section_id')->comment('섹션 ID');
$table->string('field_name')->comment('필드명');
$table->enum('field_type', ['textbox', 'number', 'dropdown', 'checkbox', 'date', 'textarea'])->comment('필드 타입');
$table->integer('order_no')->default(0)->comment('정렬 순서');
$table->boolean('is_required')->default(false)->comment('필수 여부');
$table->text('default_value')->nullable()->comment('기본값');
$table->string('placeholder')->nullable()->comment('플레이스홀더');
$table->json('display_condition')->nullable()->comment('표시 조건 {"field_id": "1", "operator": "equals", "value": "true"}');
$table->json('validation_rules')->nullable()->comment('검증 규칙 {"min": 0, "max": 100, "pattern": "regex"}');
$table->json('options')->nullable()->comment('드롭다운 옵션 [{"label": "옵션1", "value": "val1"}]');
$table->json('properties')->nullable()->comment('필드 속성 {"unit": "mm", "precision": 2, "format": "YYYY-MM-DD"}');
$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', 'section_id'], 'idx_item_fields_tenant_section');
$table->index(['section_id', 'order_no'], 'idx_item_fields_order');
// 외래키
$table->foreign('tenant_id', 'fk_item_fields_tenant')
->references('id')->on('tenants')
->onDelete('cascade');
$table->foreign('section_id', 'fk_item_fields_section')
->references('id')->on('item_sections')
->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('item_fields');
}
};

View File

@@ -0,0 +1,52 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('item_bom_items', function (Blueprint $table) {
$table->id()->comment('BOM 항목 ID');
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('section_id')->comment('섹션 ID');
$table->string('item_code', 100)->nullable()->comment('품목 코드');
$table->string('item_name')->comment('품목명');
$table->decimal('quantity', 15, 4)->default(0)->comment('수량');
$table->string('unit', 50)->nullable()->comment('단위');
$table->decimal('unit_price', 15, 2)->nullable()->comment('단가');
$table->decimal('total_price', 15, 2)->nullable()->comment('총액');
$table->text('spec')->nullable()->comment('사양');
$table->text('note')->nullable()->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', 'section_id'], 'idx_item_bom_items_tenant_section');
// 외래키
$table->foreign('tenant_id', 'fk_item_bom_items_tenant')
->references('id')->on('tenants')
->onDelete('cascade');
$table->foreign('section_id', 'fk_item_bom_items_section')
->references('id')->on('item_sections')
->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('item_bom_items');
}
};

View File

@@ -0,0 +1,45 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('custom_tabs', function (Blueprint $table) {
$table->id()->comment('커스텀 탭 ID');
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->string('label')->comment('탭 라벨');
$table->string('icon', 100)->nullable()->comment('아이콘');
$table->boolean('is_default')->default(false)->comment('기본 탭 여부');
$table->integer('order_no')->default(0)->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_custom_tabs_tenant_id');
$table->index(['tenant_id', 'order_no'], 'idx_custom_tabs_order');
// 외래키
$table->foreign('tenant_id', 'fk_custom_tabs_tenant')
->references('id')->on('tenants')
->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('custom_tabs');
}
};

View File

@@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('tab_columns', function (Blueprint $table) {
$table->id()->comment('탭 컬럼 설정 ID');
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('tab_id')->comment('탭 ID');
$table->json('columns')->comment('컬럼 설정 [{"key": "name", "label": "품목명", "visible": true, "order": 0}]');
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID');
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID');
$table->timestamps();
// 인덱스
$table->unique(['tenant_id', 'tab_id'], 'uk_tab_columns_tenant_tab');
// 외래키
$table->foreign('tenant_id', 'fk_tab_columns_tenant')
->references('id')->on('tenants')
->onDelete('cascade');
$table->foreign('tab_id', 'fk_tab_columns_tab')
->references('id')->on('custom_tabs')
->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('tab_columns');
}
};