feat: Items 테이블 통합 마이그레이션 및 문서 정리
- Items 테이블 생성 마이그레이션 추가 - ID 매핑 테이블 생성 마이그레이션 추가 - Products/Materials → Items 데이터 이관 마이그레이션 추가 - Products 테이블 BOM 컬럼 추가 마이그레이션 - item_fields unique 제약조건 수정 마이그레이션 - LOGICAL_RELATIONSHIPS.md 업데이트 - AuthApi Swagger 수정
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 논리적 데이터베이스 관계 문서
|
||||
|
||||
> **자동 생성**: 2025-12-09 11:02:18
|
||||
> **자동 생성**: 2025-12-11 21:54:15
|
||||
> **소스**: Eloquent 모델 관계 분석
|
||||
|
||||
## 📊 모델별 관계 현황
|
||||
@@ -177,6 +177,7 @@ ### main_request_flows
|
||||
### materials
|
||||
**모델**: `App\Models\Materials\Material`
|
||||
|
||||
- **category()**: belongsTo → `categories`
|
||||
- **receipts()**: hasMany → `material_receipts`
|
||||
- **lots()**: hasMany → `lots`
|
||||
- **files()**: morphMany → `files`
|
||||
|
||||
@@ -87,8 +87,8 @@ public function debugApiKey() {}
|
||||
* @OA\JsonContent(
|
||||
* required={"user_id","user_pwd"},
|
||||
*
|
||||
* @OA\Property(property="user_id", type="string", example="hamss"),
|
||||
* @OA\Property(property="user_pwd", type="string", example="StrongPass!1234")
|
||||
* @OA\Property(property="user_id", type="string", example="TestUser5"),
|
||||
* @OA\Property(property="user_pwd", type="string", example="password123!")
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
<?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.
|
||||
*
|
||||
* item_fields 테이블의 유니크 제약을 변경:
|
||||
* - 기존: UNIQUE(tenant_id, field_key)
|
||||
* - 변경: UNIQUE(tenant_id, field_key, source_table)
|
||||
*
|
||||
* 이를 통해 같은 field_key를 다른 source_table에서 사용 가능
|
||||
* 예: products.name, materials.name 모두 허용
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// 1. 유니크 인덱스 변경 (먼저!)
|
||||
Schema::table('item_fields', function (Blueprint $table) {
|
||||
// 기존 유니크 인덱스 삭제
|
||||
$table->dropUnique('uq_item_fields_tenant_field_key');
|
||||
|
||||
// 새로운 유니크 인덱스 추가 (tenant_id, field_key, source_table)
|
||||
$table->unique(
|
||||
['tenant_id', 'field_key', 'source_table'],
|
||||
'uq_item_fields_tenant_field_key_source'
|
||||
);
|
||||
});
|
||||
|
||||
// 2. 기존 데이터의 접두사 제거
|
||||
$this->removeFieldKeyPrefixes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// 1. 접두사 복원
|
||||
$this->restoreFieldKeyPrefixes();
|
||||
|
||||
// 2. 유니크 인덱스 복원
|
||||
Schema::table('item_fields', function (Blueprint $table) {
|
||||
// 새 유니크 인덱스 삭제
|
||||
$table->dropUnique('uq_item_fields_tenant_field_key_source');
|
||||
|
||||
// 기존 유니크 인덱스 복원
|
||||
$table->unique(
|
||||
['tenant_id', 'field_key'],
|
||||
'uq_item_fields_tenant_field_key'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 데이터의 접두사 제거
|
||||
*
|
||||
* materials: materials_name → name, materials_unit → unit, materials_category_id → category_id
|
||||
* material_inspections: material_inspections_quantity → quantity, material_inspections_note → note
|
||||
* material_receipts: material_receipts_lot_no → lot_no, material_receipts_quantity → quantity, material_receipts_note → note
|
||||
*/
|
||||
private function removeFieldKeyPrefixes(): void
|
||||
{
|
||||
// materials 테이블 필드
|
||||
DB::table('item_fields')
|
||||
->where('source_table', 'materials')
|
||||
->where('field_key', 'materials_name')
|
||||
->update(['field_key' => 'name']);
|
||||
|
||||
DB::table('item_fields')
|
||||
->where('source_table', 'materials')
|
||||
->where('field_key', 'materials_unit')
|
||||
->update(['field_key' => 'unit']);
|
||||
|
||||
DB::table('item_fields')
|
||||
->where('source_table', 'materials')
|
||||
->where('field_key', 'materials_category_id')
|
||||
->update(['field_key' => 'category_id']);
|
||||
|
||||
// material_inspections 테이블 필드
|
||||
DB::table('item_fields')
|
||||
->where('source_table', 'material_inspections')
|
||||
->where('field_key', 'material_inspections_quantity')
|
||||
->update(['field_key' => 'quantity']);
|
||||
|
||||
DB::table('item_fields')
|
||||
->where('source_table', 'material_inspections')
|
||||
->where('field_key', 'material_inspections_note')
|
||||
->update(['field_key' => 'note']);
|
||||
|
||||
// material_receipts 테이블 필드
|
||||
DB::table('item_fields')
|
||||
->where('source_table', 'material_receipts')
|
||||
->where('field_key', 'material_receipts_lot_no')
|
||||
->update(['field_key' => 'lot_no']);
|
||||
|
||||
DB::table('item_fields')
|
||||
->where('source_table', 'material_receipts')
|
||||
->where('field_key', 'material_receipts_quantity')
|
||||
->update(['field_key' => 'quantity']);
|
||||
|
||||
DB::table('item_fields')
|
||||
->where('source_table', 'material_receipts')
|
||||
->where('field_key', 'material_receipts_note')
|
||||
->update(['field_key' => 'note']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 접두사 복원 (롤백용)
|
||||
*/
|
||||
private function restoreFieldKeyPrefixes(): void
|
||||
{
|
||||
// materials 테이블 필드
|
||||
DB::table('item_fields')
|
||||
->where('source_table', 'materials')
|
||||
->where('field_key', 'name')
|
||||
->update(['field_key' => 'materials_name']);
|
||||
|
||||
DB::table('item_fields')
|
||||
->where('source_table', 'materials')
|
||||
->where('field_key', 'unit')
|
||||
->update(['field_key' => 'materials_unit']);
|
||||
|
||||
DB::table('item_fields')
|
||||
->where('source_table', 'materials')
|
||||
->where('field_key', 'category_id')
|
||||
->update(['field_key' => 'materials_category_id']);
|
||||
|
||||
// material_inspections 테이블 필드
|
||||
DB::table('item_fields')
|
||||
->where('source_table', 'material_inspections')
|
||||
->where('field_key', 'quantity')
|
||||
->update(['field_key' => 'material_inspections_quantity']);
|
||||
|
||||
DB::table('item_fields')
|
||||
->where('source_table', 'material_inspections')
|
||||
->where('field_key', 'note')
|
||||
->update(['field_key' => 'material_inspections_note']);
|
||||
|
||||
// material_receipts 테이블 필드
|
||||
DB::table('item_fields')
|
||||
->where('source_table', 'material_receipts')
|
||||
->where('field_key', 'lot_no')
|
||||
->update(['field_key' => 'material_receipts_lot_no']);
|
||||
|
||||
DB::table('item_fields')
|
||||
->where('source_table', 'material_receipts')
|
||||
->where('field_key', 'quantity')
|
||||
->update(['field_key' => 'material_receipts_quantity']);
|
||||
|
||||
DB::table('item_fields')
|
||||
->where('source_table', 'material_receipts')
|
||||
->where('field_key', 'note')
|
||||
->update(['field_key' => 'material_receipts_note']);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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::table('products', function (Blueprint $table) {
|
||||
$table->json('bom')->nullable()->after('options')->comment('BOM 구성품 정보 [{child_item_id, quantity}, ...]');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->dropColumn('bom');
|
||||
});
|
||||
}
|
||||
};
|
||||
103
database/migrations/2025_12_11_220000_create_items_table.php
Normal file
103
database/migrations/2025_12_11_220000_create_items_table.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Items 통합 테이블 생성
|
||||
*
|
||||
* Products + Materials 테이블을 단일 items 테이블로 통합
|
||||
* - FG, PT (Products) + SM, RM, CS (Materials) → item_type으로 구분
|
||||
* - BOM에서 child_item_type 불필요 (ID만으로 유일 식별)
|
||||
* - 단일 쿼리로 모든 품목 조회 가능
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
|
||||
// 기본 정보
|
||||
$table->string('item_type', 10)->comment('품목 유형: FG, PT, SM, RM, CS');
|
||||
$table->string('code', 100)->comment('품목 코드');
|
||||
$table->string('name', 255)->comment('품목명');
|
||||
$table->string('unit', 20)->nullable()->comment('단위');
|
||||
$table->unsignedBigInteger('category_id')->nullable()->comment('카테고리 ID');
|
||||
|
||||
// 상세 정보
|
||||
$table->text('specification')->nullable()->comment('규격 (Materials용)');
|
||||
$table->text('description')->nullable()->comment('설명');
|
||||
|
||||
// JSON 필드
|
||||
$table->json('attributes')->nullable()->comment('동적 속성');
|
||||
$table->json('attributes_archive')->nullable()->comment('속성 아카이브');
|
||||
$table->json('options')->nullable()->comment('옵션 [{label, value, unit}]');
|
||||
$table->json('bom')->nullable()->comment('BOM [{child_item_id, quantity}]');
|
||||
|
||||
// Products 전용 필드
|
||||
$table->boolean('is_sellable')->default(true)->comment('판매 가능');
|
||||
$table->boolean('is_purchasable')->default(false)->comment('구매 가능');
|
||||
$table->boolean('is_producible')->default(false)->comment('생산 가능');
|
||||
$table->integer('safety_stock')->nullable()->comment('안전 재고');
|
||||
$table->integer('lead_time')->nullable()->comment('리드타임 (일)');
|
||||
$table->boolean('is_variable_size')->default(false)->comment('가변 사이즈 여부');
|
||||
$table->string('product_category', 50)->nullable()->comment('제품 카테고리');
|
||||
$table->string('part_type', 50)->nullable()->comment('부품 유형');
|
||||
|
||||
// 파일 필드 (Products)
|
||||
$table->string('bending_diagram', 255)->nullable()->comment('절곡 도면 파일');
|
||||
$table->json('bending_details')->nullable()->comment('절곡 상세 정보');
|
||||
$table->string('specification_file', 255)->nullable()->comment('시방서 파일');
|
||||
$table->string('specification_file_name', 255)->nullable()->comment('시방서 파일명');
|
||||
$table->string('certification_file', 255)->nullable()->comment('인증서 파일');
|
||||
$table->string('certification_file_name', 255)->nullable()->comment('인증서 파일명');
|
||||
$table->string('certification_number', 100)->nullable()->comment('인증 번호');
|
||||
$table->date('certification_start_date')->nullable()->comment('인증 시작일');
|
||||
$table->date('certification_end_date')->nullable()->comment('인증 종료일');
|
||||
|
||||
// Materials 전용 필드
|
||||
$table->string('item_name', 255)->nullable()->comment('품명 (Materials)');
|
||||
$table->string('is_inspection', 1)->default('N')->comment('검사 대상 여부');
|
||||
$table->string('search_tag', 255)->nullable()->comment('검색 태그');
|
||||
$table->text('remarks')->nullable()->comment('비고');
|
||||
|
||||
// 레거시 참조 (전환기간용)
|
||||
$table->string('legacy_table', 20)->nullable()->comment('원본 테이블: products | materials');
|
||||
$table->unsignedBigInteger('legacy_id')->nullable()->comment('원본 테이블 ID');
|
||||
|
||||
// 공통 필드
|
||||
$table->boolean('is_active')->default(true)->comment('활성 상태');
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자');
|
||||
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자');
|
||||
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// 인덱스
|
||||
$table->index(['tenant_id', 'item_type'], 'idx_items_tenant_type');
|
||||
$table->index(['tenant_id', 'code'], 'idx_items_tenant_code');
|
||||
$table->index(['tenant_id', 'category_id'], 'idx_items_tenant_category');
|
||||
$table->index(['tenant_id', 'is_active'], 'idx_items_tenant_active');
|
||||
$table->index(['legacy_table', 'legacy_id'], 'idx_items_legacy');
|
||||
|
||||
// 유니크 제약 (tenant_id + code + deleted_at)
|
||||
// MySQL에서는 deleted_at이 NULL인 경우를 위해 함수 인덱스 대신 별도 처리
|
||||
$table->unique(['tenant_id', 'code', 'deleted_at'], 'uq_items_tenant_code');
|
||||
|
||||
// 외래키
|
||||
$table->foreign('tenant_id')->references('id')->on('tenants');
|
||||
$table->foreign('category_id')->references('id')->on('categories')->onDelete('set null');
|
||||
});
|
||||
|
||||
// 테이블 코멘트
|
||||
\DB::statement("ALTER TABLE items COMMENT = 'Products + Materials 통합 품목 테이블'");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('items');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Item ID 매핑 테이블 생성
|
||||
*
|
||||
* Products/Materials → Items 전환 시 ID 매핑 정보 저장
|
||||
* - 기존 API 호환성 유지를 위해 legacy ID → new ID 변환에 사용
|
||||
* - 전환 완료 후 삭제 예정
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('item_id_mappings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('new_item_id')->comment('새 items 테이블 ID');
|
||||
$table->string('legacy_table', 20)->comment('원본 테이블: products | materials');
|
||||
$table->unsignedBigInteger('legacy_id')->comment('원본 테이블 ID');
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
// 인덱스
|
||||
$table->unique(['legacy_table', 'legacy_id'], 'uq_legacy');
|
||||
$table->index('new_item_id', 'idx_new_item');
|
||||
|
||||
// 외래키
|
||||
$table->foreign('new_item_id')->references('id')->on('items')->onDelete('cascade');
|
||||
});
|
||||
|
||||
// 테이블 코멘트
|
||||
\DB::statement("ALTER TABLE item_id_mappings COMMENT = 'Products/Materials → Items ID 매핑 (전환기간용)'");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('item_id_mappings');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Products/Materials 데이터를 Items 테이블로 이관
|
||||
*
|
||||
* 1. Products 테이블 데이터 → Items 이관
|
||||
* 2. Materials 테이블 데이터 → Items 이관
|
||||
* 3. ID 매핑 테이블 생성
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// 1. Products → Items 이관
|
||||
echo "Products 데이터 이관 시작...\n";
|
||||
|
||||
$productsBefore = DB::table('products')->count();
|
||||
echo "- Products 원본: {$productsBefore}건\n";
|
||||
|
||||
DB::statement("
|
||||
INSERT INTO items (
|
||||
tenant_id, item_type, code, name, unit, category_id,
|
||||
description, attributes, attributes_archive, options, bom,
|
||||
is_sellable, is_purchasable, is_producible,
|
||||
safety_stock, lead_time, is_variable_size, product_category, part_type,
|
||||
bending_diagram, bending_details, specification_file, specification_file_name,
|
||||
certification_file, certification_file_name, certification_number,
|
||||
certification_start_date, certification_end_date,
|
||||
is_active, created_by, updated_by, deleted_by,
|
||||
created_at, updated_at, deleted_at,
|
||||
legacy_table, legacy_id
|
||||
)
|
||||
SELECT
|
||||
tenant_id, product_type, code, name, unit, category_id,
|
||||
description, attributes, attributes_archive, options, bom,
|
||||
is_sellable, is_purchasable, is_producible,
|
||||
safety_stock, lead_time, is_variable_size, product_category, part_type,
|
||||
bending_diagram, bending_details, specification_file, specification_file_name,
|
||||
certification_file, certification_file_name, certification_number,
|
||||
certification_start_date, certification_end_date,
|
||||
is_active, created_by, updated_by, deleted_by,
|
||||
created_at, updated_at, deleted_at,
|
||||
'products', id
|
||||
FROM products
|
||||
");
|
||||
|
||||
$productsAfter = DB::table('items')->where('legacy_table', 'products')->count();
|
||||
echo "- Items로 이관: {$productsAfter}건\n";
|
||||
|
||||
// 2. Materials → Items 이관
|
||||
echo "\nMaterials 데이터 이관 시작...\n";
|
||||
|
||||
$materialsBefore = DB::table('materials')->count();
|
||||
echo "- Materials 원본: {$materialsBefore}건\n";
|
||||
|
||||
DB::statement("
|
||||
INSERT INTO items (
|
||||
tenant_id, item_type, code, name, unit, category_id,
|
||||
specification, attributes, options,
|
||||
item_name, is_inspection, search_tag, remarks,
|
||||
is_active, created_by, updated_by, deleted_by,
|
||||
created_at, updated_at, deleted_at,
|
||||
legacy_table, legacy_id
|
||||
)
|
||||
SELECT
|
||||
tenant_id, material_type, material_code, name, unit, category_id,
|
||||
specification, attributes, options,
|
||||
item_name, is_inspection, search_tag, remarks,
|
||||
is_active, created_by, updated_by, deleted_by,
|
||||
created_at, updated_at, deleted_at,
|
||||
'materials', id
|
||||
FROM materials
|
||||
");
|
||||
|
||||
$materialsAfter = DB::table('items')->where('legacy_table', 'materials')->count();
|
||||
echo "- Items로 이관: {$materialsAfter}건\n";
|
||||
|
||||
// 3. ID 매핑 테이블 생성
|
||||
echo "\nID 매핑 테이블 생성 시작...\n";
|
||||
|
||||
DB::statement("
|
||||
INSERT INTO item_id_mappings (new_item_id, legacy_table, legacy_id)
|
||||
SELECT id, legacy_table, legacy_id
|
||||
FROM items
|
||||
WHERE legacy_table IS NOT NULL AND legacy_id IS NOT NULL
|
||||
");
|
||||
|
||||
$mappingsCount = DB::table('item_id_mappings')->count();
|
||||
echo "- ID 매핑: {$mappingsCount}건 생성\n";
|
||||
|
||||
// 4. 검증
|
||||
echo "\n=== 이관 검증 ===\n";
|
||||
$totalItems = DB::table('items')->count();
|
||||
$totalOriginal = $productsBefore + $materialsBefore;
|
||||
echo "- 원본 합계: {$totalOriginal}건 (Products: {$productsBefore} + Materials: {$materialsBefore})\n";
|
||||
echo "- Items 합계: {$totalItems}건\n";
|
||||
|
||||
if ($totalItems === $totalOriginal) {
|
||||
echo "✅ 데이터 이관 성공!\n";
|
||||
} else {
|
||||
echo "⚠️ 데이터 불일치! 확인 필요\n";
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// 롤백: Items 테이블 데이터 삭제 (매핑 테이블은 CASCADE로 자동 삭제)
|
||||
echo "Items 데이터 롤백 시작...\n";
|
||||
|
||||
$countBefore = DB::table('items')->count();
|
||||
DB::table('items')->truncate();
|
||||
$countAfter = DB::table('items')->count();
|
||||
|
||||
echo "- 삭제된 Items: {$countBefore}건\n";
|
||||
echo "- 롤백 완료\n";
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user