feat: item_fields에 field_key, is_locked 컬럼 추가

- field_key: {ID}_{key} 형식으로 고유키 생성
- is_locked, locked_by, locked_at 잠금 컬럼 추가
- ItemFieldService: store/update/clone 로직 수정
- FormRequest: field_key 검증 규칙 추가
- Swagger 스키마 업데이트
This commit is contained in:
2025-11-28 17:39:14 +09:00
parent d3fb00ae26
commit aa2962314f
7 changed files with 144 additions and 8 deletions

View File

@@ -16,6 +16,7 @@ public function rules(): array
return [
'group_id' => 'nullable|integer',
'field_name' => 'required|string|max:255',
'field_key' => 'nullable|string|max:80|regex:/^[a-zA-Z][a-zA-Z0-9_]*$/',
'field_type' => 'required|in:textbox,number,dropdown,checkbox,date,textarea',
'is_required' => 'nullable|boolean',
'default_value' => 'nullable|string',
@@ -24,6 +25,14 @@ public function rules(): array
'validation_rules' => 'nullable|array',
'options' => 'nullable|array',
'properties' => 'nullable|array',
'is_locked' => 'nullable|boolean',
];
}
public function messages(): array
{
return [
'field_key.regex' => __('validation.field_key_format'),
];
}
}

View File

@@ -16,6 +16,7 @@ public function rules(): array
return [
'group_id' => 'nullable|integer|min:1', // 계층번호
'field_name' => 'required|string|max:255',
'field_key' => 'nullable|string|max:80|regex:/^[a-zA-Z][a-zA-Z0-9_]*$/',
'field_type' => 'required|in:textbox,number,dropdown,checkbox,date,textarea',
'is_required' => 'nullable|boolean',
'default_value' => 'nullable|string',
@@ -24,6 +25,14 @@ public function rules(): array
'validation_rules' => 'nullable|array',
'options' => 'nullable|array',
'properties' => 'nullable|array',
'is_locked' => 'nullable|boolean',
];
}
public function messages(): array
{
return [
'field_key.regex' => __('validation.field_key_format'),
];
}
}

View File

@@ -15,6 +15,7 @@ public function rules(): array
{
return [
'field_name' => 'sometimes|string|max:255',
'field_key' => 'nullable|string|max:80|regex:/^[a-zA-Z][a-zA-Z0-9_]*$/',
'field_type' => 'sometimes|in:textbox,number,dropdown,checkbox,date,textarea',
'is_required' => 'nullable|boolean',
'default_value' => 'nullable|string',
@@ -23,6 +24,14 @@ public function rules(): array
'validation_rules' => 'nullable|array',
'options' => 'nullable|array',
'properties' => 'nullable|array',
'is_locked' => 'nullable|boolean',
];
}
public function messages(): array
{
return [
'field_key.regex' => __('validation.field_key_format'),
];
}
}

View File

@@ -15,6 +15,7 @@ class ItemField extends Model
'tenant_id',
'group_id',
'field_name',
'field_key',
'field_type',
'order_no',
'is_required',
@@ -27,6 +28,9 @@ class ItemField extends Model
'category',
'description',
'is_common',
'is_locked',
'locked_by',
'locked_at',
'created_by',
'updated_by',
'deleted_by',
@@ -37,6 +41,7 @@ class ItemField extends Model
'order_no' => 'integer',
'is_required' => 'boolean',
'is_common' => 'boolean',
'is_locked' => 'boolean',
'display_condition' => 'array',
'validation_rules' => 'array',
'options' => 'array',
@@ -44,6 +49,7 @@ class ItemField extends Model
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
'locked_at' => 'datetime',
];
protected $hidden = [

View File

@@ -37,6 +37,7 @@ public function storeIndependent(array $data): ItemField
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
// 1. 필드 생성 (field_key 없이)
$field = ItemField::create([
'tenant_id' => $tenantId,
'group_id' => $data['group_id'] ?? 1,
@@ -53,9 +54,17 @@ public function storeIndependent(array $data): ItemField
'category' => $data['category'] ?? null,
'description' => $data['description'] ?? null,
'is_common' => $data['is_common'] ?? false,
'is_locked' => $data['is_locked'] ?? false,
'locked_by' => ($data['is_locked'] ?? false) ? $userId : null,
'locked_at' => ($data['is_locked'] ?? false) ? now() : null,
'created_by' => $userId,
]);
// 2. field_key 설정 ({ID}_{field_key} 형식)
if (! empty($data['field_key'])) {
$field->update(['field_key' => $field->id.'_'.$data['field_key']]);
}
return $field;
}
@@ -77,6 +86,7 @@ public function clone(int $id): ItemField
throw new NotFoundHttpException(__('error.field_not_found'));
}
// 1. 복제 생성 (field_key, is_locked 없이)
$cloned = ItemField::create([
'tenant_id' => $tenantId,
'group_id' => $original->group_id,
@@ -93,9 +103,17 @@ public function clone(int $id): ItemField
'category' => $original->category,
'description' => $original->description,
'is_common' => $original->is_common,
'is_locked' => false, // 복제본은 잠금 해제 상태
'created_by' => $userId,
]);
// 2. field_key 복제 ({새ID}_{원본key} 형식)
if ($original->field_key) {
// 원본 field_key에서 키 부분 추출 (예: "81_itemNum" → "itemNum")
$originalKey = preg_replace('/^\d+_/', '', $original->field_key);
$cloned->update(['field_key' => $cloned->id.'_'.$originalKey.'_copy']);
}
return $cloned;
}
@@ -142,7 +160,7 @@ public function store(int $sectionId, array $data): ItemField
->where('child_type', EntityRelationship::TYPE_FIELD)
->max('order_no');
// 필드 생성
// 1. 필드 생성
$field = ItemField::create([
'tenant_id' => $tenantId,
'group_id' => $data['group_id'] ?? 1,
@@ -156,10 +174,18 @@ public function store(int $sectionId, array $data): ItemField
'validation_rules' => $data['validation_rules'] ?? null,
'options' => $data['options'] ?? null,
'properties' => $data['properties'] ?? null,
'is_locked' => $data['is_locked'] ?? false,
'locked_by' => ($data['is_locked'] ?? false) ? $userId : null,
'locked_at' => ($data['is_locked'] ?? false) ? now() : null,
'created_by' => $userId,
]);
// 섹션-필드 관계 생성
// 2. field_key 설정 ({ID}_{field_key} 형식)
if (! empty($data['field_key'])) {
$field->update(['field_key' => $field->id.'_'.$data['field_key']]);
}
// 3. 섹션-필드 관계 생성
EntityRelationship::link(
$tenantId,
EntityRelationship::TYPE_SECTION,
@@ -190,7 +216,8 @@ public function update(int $id, array $data): ItemField
throw new NotFoundHttpException(__('error.not_found'));
}
$field->update([
// 기본 필드 업데이트
$updateData = [
'field_name' => $data['field_name'] ?? $field->field_name,
'field_type' => $data['field_type'] ?? $field->field_type,
'is_required' => $data['is_required'] ?? $field->is_required,
@@ -204,7 +231,28 @@ public function update(int $id, array $data): ItemField
'description' => $data['description'] ?? $field->description,
'is_common' => $data['is_common'] ?? $field->is_common,
'updated_by' => $userId,
]);
];
// field_key 변경 시 {ID}_{field_key} 형식 유지
if (array_key_exists('field_key', $data)) {
$updateData['field_key'] = $data['field_key'] ? $field->id.'_'.$data['field_key'] : null;
}
// is_locked 변경 처리
if (array_key_exists('is_locked', $data)) {
$updateData['is_locked'] = $data['is_locked'];
if ($data['is_locked'] && ! $field->is_locked) {
// 새로 잠금 설정
$updateData['locked_by'] = $userId;
$updateData['locked_at'] = now();
} elseif (! $data['is_locked'] && $field->is_locked) {
// 잠금 해제
$updateData['locked_by'] = null;
$updateData['locked_at'] = null;
}
}
$field->update($updateData);
return $field;
}

View File

@@ -70,10 +70,13 @@
* @OA\Property(property="tenant_id", type="integer", example=1),
* @OA\Property(property="group_id", type="integer", nullable=true, example=1, description="계층번호"),
* @OA\Property(property="field_name", type="string", example="제품명"),
* @OA\Property(property="field_key", type="string", nullable=true, example="81_itemNum", description="필드 고유 키 ({ID}_{key} 형식)"),
* @OA\Property(property="field_type", type="string", enum={"textbox","number","dropdown","checkbox","date","textarea"}, example="textbox"),
* @OA\Property(property="order_no", type="integer", example=0),
* @OA\Property(property="is_required", type="boolean", example=true),
* @OA\Property(property="is_locked", type="boolean", example=false, description="연결 잠금 여부 (init API 응답 시 포함)"),
* @OA\Property(property="is_locked", type="boolean", example=false, description="엔티티 잠금 여부"),
* @OA\Property(property="locked_by", type="integer", nullable=true, example=null, description="잠금 설정자 ID"),
* @OA\Property(property="locked_at", type="string", nullable=true, example=null, description="잠금 설정 일시"),
* @OA\Property(property="default_value", type="string", nullable=true, example=null),
* @OA\Property(property="placeholder", type="string", nullable=true, example="제품명을 입력하세요"),
* @OA\Property(property="display_condition", type="object", nullable=true, example=null),
@@ -201,6 +204,7 @@
*
* @OA\Property(property="group_id", type="integer", nullable=true, example=1, description="계층번호"),
* @OA\Property(property="field_name", type="string", maxLength=255, example="제품명"),
* @OA\Property(property="field_key", type="string", nullable=true, maxLength=80, example="itemNum", description="필드 고유 키 (영문+숫자+언더스코어, 저장 시 {ID}_{key} 형식으로 변환)"),
* @OA\Property(property="field_type", type="string", enum={"textbox","number","dropdown","checkbox","date","textarea"}, example="textbox"),
* @OA\Property(property="is_required", type="boolean", example=false),
* @OA\Property(property="default_value", type="string", nullable=true, example=null),
@@ -208,7 +212,8 @@
* @OA\Property(property="display_condition", type="object", nullable=true, example=null),
* @OA\Property(property="validation_rules", type="object", nullable=true, example=null),
* @OA\Property(property="options", type="object", nullable=true, example=null),
* @OA\Property(property="properties", type="object", nullable=true, example=null)
* @OA\Property(property="properties", type="object", nullable=true, example=null),
* @OA\Property(property="is_locked", type="boolean", nullable=true, example=false, description="잠금 여부")
* )
*
* @OA\Schema(
@@ -262,6 +267,7 @@
*
* @OA\Property(property="group_id", type="integer", nullable=true, example=1, description="계층번호"),
* @OA\Property(property="field_name", type="string", maxLength=255, example="제품명"),
* @OA\Property(property="field_key", type="string", nullable=true, maxLength=80, example="itemNum", description="필드 고유 키 (영문+숫자+언더스코어, 저장 시 {ID}_{key} 형식으로 변환)"),
* @OA\Property(property="field_type", type="string", enum={"textbox","number","dropdown","checkbox","date","textarea"}, example="textbox"),
* @OA\Property(property="is_required", type="boolean", example=true),
* @OA\Property(property="default_value", type="string", nullable=true, example=null),
@@ -269,7 +275,8 @@
* @OA\Property(property="display_condition", type="object", nullable=true, example=null),
* @OA\Property(property="validation_rules", type="object", nullable=true, example=null),
* @OA\Property(property="options", type="object", nullable=true, example=null),
* @OA\Property(property="properties", type="object", nullable=true, example=null)
* @OA\Property(property="properties", type="object", nullable=true, example=null),
* @OA\Property(property="is_locked", type="boolean", nullable=true, example=false, description="잠금 여부")
* )
*
* @OA\Schema(
@@ -277,6 +284,7 @@
* type="object",
*
* @OA\Property(property="field_name", type="string", maxLength=255, example="제품명"),
* @OA\Property(property="field_key", type="string", nullable=true, maxLength=80, example="itemNum", description="필드 고유 키 (영문+숫자+언더스코어)"),
* @OA\Property(property="field_type", type="string", enum={"textbox","number","dropdown","checkbox","date","textarea"}, example="textbox"),
* @OA\Property(property="is_required", type="boolean", example=true),
* @OA\Property(property="default_value", type="string", nullable=true, example=null),
@@ -284,7 +292,8 @@
* @OA\Property(property="display_condition", type="object", nullable=true, example=null),
* @OA\Property(property="validation_rules", type="object", nullable=true, example=null),
* @OA\Property(property="options", type="object", nullable=true, example=null),
* @OA\Property(property="properties", type="object", nullable=true, example=null)
* @OA\Property(property="properties", type="object", nullable=true, example=null),
* @OA\Property(property="is_locked", type="boolean", nullable=true, example=false, description="잠금 여부")
* )
*
* @OA\Schema(

View File

@@ -0,0 +1,46 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* item_fields 테이블에 field_key 및 잠금 컬럼 추가
*
* 변경 내용:
* 1. field_key: 필드 고유 식별자 ({ID}_{key} 형식, unique)
* 2. is_locked: 엔티티 자체 잠금 여부
* 3. locked_by: 잠금 설정자 ID
* 4. locked_at: 잠금 설정 일시
*/
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('item_fields', function (Blueprint $table) {
$table->string('field_key', 100)->nullable()->after('field_name')->comment('필드 고유 키 ({ID}_{key} 형식)');
$table->boolean('is_locked')->default(false)->after('is_common')->comment('잠금 여부');
$table->unsignedBigInteger('locked_by')->nullable()->after('is_locked')->comment('잠금 설정자 ID');
$table->timestamp('locked_at')->nullable()->after('locked_by')->comment('잠금 설정 일시');
// 인덱스
$table->unique(['tenant_id', 'field_key'], 'uq_item_fields_tenant_field_key');
$table->index('is_locked', 'idx_item_fields_is_locked');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('item_fields', function (Blueprint $table) {
$table->dropUnique('uq_item_fields_tenant_field_key');
$table->dropIndex('idx_item_fields_is_locked');
$table->dropColumn(['field_key', 'is_locked', 'locked_by', 'locked_at']);
});
}
};