feat(item-master): 잠금 기능 추가 및 FK 레거시 코드 정리
## 잠금 기능 (Lock Feature) - entity_relationships 테이블에 is_locked, locked_by, locked_at 컬럼 추가 - EntityRelationship 모델에 잠금 관련 헬퍼 메서드 추가 - LockCheckTrait 생성 (destroy 시 잠금 체크 공통 로직) - 각 Service의 destroy() 메서드에 잠금 체크 적용 - API 응답에 is_locked 필드 포함 - 한국어 에러 메시지 추가 ## FK 레거시 코드 정리 - ItemMasterSeeder: entity_relationships 기반으로 전환 - ItemPage 모델: FK 기반 sections() 관계 제거 - ItemSectionService: clone() 메서드 FK 제거 - SectionTemplateService: page_id 컬럼 참조 제거 - EntityRelationship::link() 파라미터 순서 통일 ## 기타 - Swagger 스키마에 is_locked 속성 추가 - 프론트엔드 가이드 문서 추가
This commit is contained in:
@@ -39,4 +39,4 @@ public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('unit_options');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -41,4 +41,4 @@ public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('section_templates');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -47,4 +47,4 @@ public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('item_master_fields');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -42,4 +42,4 @@ public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('item_pages');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -45,4 +45,4 @@ public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('item_sections');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -52,4 +52,4 @@ public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('item_fields');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -49,4 +49,4 @@ public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('item_bom_items');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -42,4 +42,4 @@ public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('custom_tabs');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -40,4 +40,4 @@ public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('tab_columns');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -65,13 +65,13 @@ public function up(): void
|
||||
];
|
||||
|
||||
foreach ($tables as $tableName) {
|
||||
if (!Schema::hasTable($tableName) || in_array($tableName, $excludeTables)) {
|
||||
if (! Schema::hasTable($tableName) || in_array($tableName, $excludeTables)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Schema::table($tableName, function (Blueprint $table) use ($tableName) {
|
||||
// created_by 추가
|
||||
if (!Schema::hasColumn($tableName, 'created_by')) {
|
||||
if (! Schema::hasColumn($tableName, 'created_by')) {
|
||||
// updated_at 다음에 추가
|
||||
if (Schema::hasColumn($tableName, 'updated_at')) {
|
||||
$table->foreignId('created_by')
|
||||
@@ -86,7 +86,7 @@ public function up(): void
|
||||
}
|
||||
|
||||
// updated_by 추가
|
||||
if (!Schema::hasColumn($tableName, 'updated_by')) {
|
||||
if (! Schema::hasColumn($tableName, 'updated_by')) {
|
||||
// created_by 다음에 추가
|
||||
if (Schema::hasColumn($tableName, 'created_by')) {
|
||||
$table->foreignId('updated_by')
|
||||
@@ -159,7 +159,7 @@ public function down(): void
|
||||
];
|
||||
|
||||
foreach ($tables as $tableName) {
|
||||
if (!Schema::hasTable($tableName) || in_array($tableName, $excludeTables)) {
|
||||
if (! Schema::hasTable($tableName) || in_array($tableName, $excludeTables)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -176,4 +176,4 @@ public function down(): void
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -47,12 +47,12 @@ public function up(): void
|
||||
];
|
||||
|
||||
foreach ($tables as $tableName) {
|
||||
if (!Schema::hasTable($tableName)) {
|
||||
if (! Schema::hasTable($tableName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Schema::table($tableName, function (Blueprint $table) use ($tableName) {
|
||||
if (!Schema::hasColumn($tableName, 'deleted_by')) {
|
||||
if (! Schema::hasColumn($tableName, 'deleted_by')) {
|
||||
$table->foreignId('deleted_by')
|
||||
->nullable()
|
||||
->after('deleted_at')
|
||||
@@ -101,7 +101,7 @@ public function down(): void
|
||||
];
|
||||
|
||||
foreach ($tables as $tableName) {
|
||||
if (!Schema::hasTable($tableName)) {
|
||||
if (! Schema::hasTable($tableName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -112,4 +112,4 @@ public function down(): void
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -31,4 +31,4 @@ public function down(): void
|
||||
$table->unsignedBigInteger('page_id')->nullable(false)->comment('페이지 ID')->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* 연결(관계) 잠금 기능 추가:
|
||||
* - is_locked=true인 연결은 해제 불가
|
||||
* - 잠금된 연결의 자식 엔티티도 삭제 불가 (연결을 통한 보호)
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('entity_relationships', function (Blueprint $table) {
|
||||
$table->boolean('is_locked')->default(false)->after('metadata')->comment('잠금 여부 (잠금 시 연결 해제 및 자식 삭제 불가)');
|
||||
$table->unsignedBigInteger('locked_by')->nullable()->after('is_locked')->comment('잠금 설정자 ID');
|
||||
$table->timestamp('locked_at')->nullable()->after('locked_by')->comment('잠금 설정 일시');
|
||||
|
||||
$table->index('is_locked', 'idx_entity_rel_is_locked');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('entity_relationships', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_entity_rel_is_locked');
|
||||
$table->dropColumn(['is_locked', 'locked_by', 'locked_at']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\ItemMaster\UnitOption;
|
||||
use App\Models\ItemMaster\SectionTemplate;
|
||||
use App\Models\ItemMaster\ItemMasterField;
|
||||
use App\Models\ItemMaster\CustomTab;
|
||||
use App\Models\ItemMaster\EntityRelationship;
|
||||
use App\Models\ItemMaster\ItemBomItem;
|
||||
use App\Models\ItemMaster\ItemField;
|
||||
use App\Models\ItemMaster\ItemPage;
|
||||
use App\Models\ItemMaster\ItemSection;
|
||||
use App\Models\ItemMaster\ItemField;
|
||||
use App\Models\ItemMaster\ItemBomItem;
|
||||
use App\Models\ItemMaster\CustomTab;
|
||||
use App\Models\ItemMaster\SectionTemplate;
|
||||
use App\Models\ItemMaster\TabColumn;
|
||||
use App\Models\ItemMaster\UnitOption;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class ItemMasterSeeder extends Seeder
|
||||
@@ -22,6 +22,7 @@ public function run(): void
|
||||
{
|
||||
$tenantId = 1; // 기본 테넌트 ID (실제 환경에 맞게 조정)
|
||||
$userId = 1; // 기본 사용자 ID (실제 환경에 맞게 조정)
|
||||
$groupId = 1; // 기본 그룹 ID
|
||||
|
||||
// 1. 단위 옵션
|
||||
$units = [
|
||||
@@ -60,7 +61,7 @@ public function run(): void
|
||||
]);
|
||||
}
|
||||
|
||||
// 3. 마스터 필드
|
||||
// 3. 마스터 필드 (독립 엔티티로 생성)
|
||||
$masterFields = [
|
||||
['field_name' => '제품명', 'field_type' => 'textbox', 'category' => '기본정보', 'is_common' => true],
|
||||
['field_name' => '제품코드', 'field_type' => 'textbox', 'category' => '기본정보', 'is_common' => true],
|
||||
@@ -77,10 +78,13 @@ public function run(): void
|
||||
];
|
||||
|
||||
foreach ($masterFields as $field) {
|
||||
ItemMasterField::create([
|
||||
ItemField::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'group_id' => $groupId,
|
||||
'field_name' => $field['field_name'],
|
||||
'field_type' => $field['field_type'],
|
||||
'order_no' => 0,
|
||||
'is_required' => false,
|
||||
'category' => $field['category'],
|
||||
'is_common' => $field['is_common'],
|
||||
'options' => $field['options'] ?? null,
|
||||
@@ -99,8 +103,10 @@ public function run(): void
|
||||
];
|
||||
|
||||
foreach ($pages as $index => $page) {
|
||||
// 페이지 생성 (독립 엔티티)
|
||||
$itemPage = ItemPage::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'group_id' => $groupId,
|
||||
'page_name' => $page['page_name'],
|
||||
'item_type' => $page['item_type'],
|
||||
'absolute_path' => $page['absolute_path'],
|
||||
@@ -108,65 +114,89 @@ public function run(): void
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
|
||||
// 각 페이지에 기본 섹션 추가
|
||||
// 기본 정보 섹션 생성 (독립 엔티티)
|
||||
$section1 = ItemSection::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'page_id' => $itemPage->id,
|
||||
'group_id' => $groupId,
|
||||
'title' => '기본 정보',
|
||||
'type' => 'fields',
|
||||
'order_no' => 0,
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
|
||||
// 섹션에 필드 추가
|
||||
ItemField::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'section_id' => $section1->id,
|
||||
'field_name' => '제품명',
|
||||
'field_type' => 'textbox',
|
||||
'order_no' => 0,
|
||||
'is_required' => true,
|
||||
'placeholder' => '제품명을 입력하세요',
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
// 페이지-섹션 관계 생성 (entity_relationships)
|
||||
// link(tenantId, parentType, parentId, childType, childId, orderNo, metadata, groupId)
|
||||
EntityRelationship::link(
|
||||
$tenantId,
|
||||
EntityRelationship::TYPE_PAGE,
|
||||
$itemPage->id,
|
||||
EntityRelationship::TYPE_SECTION,
|
||||
$section1->id,
|
||||
0, // order_no
|
||||
null, // metadata
|
||||
$groupId
|
||||
);
|
||||
|
||||
ItemField::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'section_id' => $section1->id,
|
||||
'field_name' => '제품코드',
|
||||
'field_type' => 'textbox',
|
||||
'order_no' => 1,
|
||||
'is_required' => true,
|
||||
'placeholder' => '제품코드를 입력하세요',
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
// 필드 생성 및 섹션에 연결
|
||||
$fieldData = [
|
||||
['field_name' => '제품명', 'is_required' => true, 'placeholder' => '제품명을 입력하세요'],
|
||||
['field_name' => '제품코드', 'is_required' => true, 'placeholder' => '제품코드를 입력하세요'],
|
||||
['field_name' => '규격', 'is_required' => false, 'placeholder' => '규격을 입력하세요'],
|
||||
];
|
||||
|
||||
ItemField::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'section_id' => $section1->id,
|
||||
'field_name' => '규격',
|
||||
'field_type' => 'textbox',
|
||||
'order_no' => 2,
|
||||
'is_required' => false,
|
||||
'placeholder' => '규격을 입력하세요',
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
foreach ($fieldData as $orderNo => $fd) {
|
||||
$field = ItemField::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'group_id' => $groupId,
|
||||
'field_name' => $fd['field_name'],
|
||||
'field_type' => 'textbox',
|
||||
'order_no' => $orderNo,
|
||||
'is_required' => $fd['is_required'],
|
||||
'placeholder' => $fd['placeholder'],
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
|
||||
// 섹션-필드 관계 생성 (entity_relationships)
|
||||
EntityRelationship::link(
|
||||
$tenantId,
|
||||
EntityRelationship::TYPE_SECTION,
|
||||
$section1->id,
|
||||
EntityRelationship::TYPE_FIELD,
|
||||
$field->id,
|
||||
$orderNo,
|
||||
null,
|
||||
$groupId
|
||||
);
|
||||
}
|
||||
|
||||
// BOM 섹션 (완제품, 반제품만)
|
||||
if (in_array($page['item_type'], ['FG', 'PT'])) {
|
||||
// BOM 섹션 생성 (독립 엔티티)
|
||||
$section2 = ItemSection::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'page_id' => $itemPage->id,
|
||||
'group_id' => $groupId,
|
||||
'title' => 'BOM 구성',
|
||||
'type' => 'bom',
|
||||
'order_no' => 1,
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
|
||||
// BOM 항목 샘플
|
||||
ItemBomItem::create([
|
||||
// 페이지-섹션 관계 생성
|
||||
EntityRelationship::link(
|
||||
$tenantId,
|
||||
EntityRelationship::TYPE_PAGE,
|
||||
$itemPage->id,
|
||||
EntityRelationship::TYPE_SECTION,
|
||||
$section2->id,
|
||||
1, // order_no
|
||||
null,
|
||||
$groupId
|
||||
);
|
||||
|
||||
// BOM 항목 생성 (독립 엔티티)
|
||||
$bomItem = ItemBomItem::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'section_id' => $section2->id,
|
||||
'group_id' => $groupId,
|
||||
'item_code' => 'MAT-001',
|
||||
'item_name' => '철판',
|
||||
'quantity' => 10.5,
|
||||
@@ -177,6 +207,18 @@ public function run(): void
|
||||
'note' => '샘플 BOM 항목',
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
|
||||
// 섹션-BOM 관계 생성
|
||||
EntityRelationship::link(
|
||||
$tenantId,
|
||||
EntityRelationship::TYPE_SECTION,
|
||||
$section2->id,
|
||||
EntityRelationship::TYPE_BOM,
|
||||
$bomItem->id,
|
||||
0,
|
||||
null,
|
||||
$groupId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,10 +254,10 @@ public function run(): void
|
||||
}
|
||||
|
||||
echo "✅ ItemMaster 시드 데이터 생성 완료!\n";
|
||||
echo " - 단위 옵션: ".count($units)."개\n";
|
||||
echo " - 섹션 템플릿: ".count($templates)."개\n";
|
||||
echo " - 마스터 필드: ".count($masterFields)."개\n";
|
||||
echo " - 품목 페이지: ".count($pages)."개\n";
|
||||
echo " - 커스텀 탭: ".count($tabs)."개\n";
|
||||
echo ' - 단위 옵션: '.count($units)."개\n";
|
||||
echo ' - 섹션 템플릿: '.count($templates)."개\n";
|
||||
echo ' - 마스터 필드: '.count($masterFields)."개\n";
|
||||
echo ' - 품목 페이지: '.count($pages)."개\n";
|
||||
echo ' - 커스텀 탭: '.count($tabs)."개\n";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user