fix : Category, Product 관련 테이블 정리 (Models, DB, seeder)
This commit is contained in:
@@ -3,44 +3,37 @@
|
||||
namespace App\Models\Commons;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* @mixin IdeHelperCategory
|
||||
*/
|
||||
class Category extends Model
|
||||
{
|
||||
use SoftDeletes, BelongsToTenant;
|
||||
|
||||
protected $table = 'common_codes';
|
||||
use SoftDeletes, BelongsToTenant, ModelTrait;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'code_group',
|
||||
'code',
|
||||
'name',
|
||||
'parent_id',
|
||||
'attributes',
|
||||
'description',
|
||||
'is_active',
|
||||
'sort_order'
|
||||
'tenant_id','parent_id','code_group','code','name',
|
||||
'profile_code', // capability_profile 연결
|
||||
'is_active','sort_order','description',
|
||||
'created_by','updated_by','deleted_by'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'attributes' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
// 관계: 상위 코드
|
||||
public function parent()
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_id');
|
||||
}
|
||||
// 계층
|
||||
public function parent() { return $this->belongsTo(self::class, 'parent_id'); }
|
||||
public function children() { return $this->hasMany(self::class, 'parent_id'); }
|
||||
|
||||
// 관계: 하위 코드들
|
||||
public function children()
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_id');
|
||||
}
|
||||
// 카테고리의 제품들
|
||||
public function products() { return $this->hasMany(\App\Models\Products\Product::class, 'category_id'); }
|
||||
|
||||
// 태그(폴리모픽) — 이미 taggables 존재
|
||||
public function tags() { return $this->morphToMany(\App\Models\Commons\Tag::class, 'taggable'); }
|
||||
|
||||
// 스코프
|
||||
public function scopeGroup($q, string $group) { return $q->where('code_group', $group); }
|
||||
public function scopeCode($q, string $code) { return $q->where('code', $code); }
|
||||
}
|
||||
|
||||
36
app/Models/Commons/CategoryField.php
Normal file
36
app/Models/Commons/CategoryField.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Commons;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class CategoryField extends Model
|
||||
{
|
||||
use SoftDeletes, BelongsToTenant, ModelTrait;
|
||||
|
||||
protected $table = 'category_fields';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id','category_id',
|
||||
'field_key','field_name','field_type',
|
||||
'is_required','sort_order','default_value','options','description',
|
||||
'created_by','updated_by','deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_required' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
|
||||
// 편의 스코프
|
||||
public function scopeRequired($q) { return $q->where('is_required', 1); }
|
||||
}
|
||||
31
app/Models/Commons/CategoryLog.php
Normal file
31
app/Models/Commons/CategoryLog.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Commons;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CategoryLog extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait;
|
||||
|
||||
protected $table = 'category_logs';
|
||||
public $timestamps = false; // changed_at 컬럼 단일 사용
|
||||
|
||||
protected $fillable = [
|
||||
'category_id','tenant_id','action','changed_by','changed_at',
|
||||
'before_json','after_json','remarks',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'changed_at' => 'datetime',
|
||||
'before_json' => 'array',
|
||||
'after_json' => 'array',
|
||||
];
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
}
|
||||
30
app/Models/Commons/CategoryTemplate.php
Normal file
30
app/Models/Commons/CategoryTemplate.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Commons;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CategoryTemplate extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait;
|
||||
|
||||
protected $table = 'category_templates';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id','category_id','version_no','template_json','applied_at',
|
||||
'created_by','updated_by','deleted_by','remarks',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'version_no' => 'integer',
|
||||
'template_json' => 'array',
|
||||
'applied_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,13 @@
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
/**
|
||||
* @mixin IdeHelperCommonCode
|
||||
*/
|
||||
class CommonCode extends Model
|
||||
{
|
||||
use SoftDeletes, BelongsToTenant;
|
||||
use SoftDeletes, BelongsToTenant, ModelTrait;
|
||||
|
||||
protected $table = 'common_codes';
|
||||
|
||||
|
||||
@@ -2,35 +2,65 @@
|
||||
|
||||
namespace App\Models\Products;
|
||||
|
||||
use App\Models\Commons\Category;
|
||||
use App\Models\Commons\File;
|
||||
use App\Models\Commons\Tag;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* @mixin IdeHelperProduct
|
||||
*/
|
||||
class Product extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
protected $fillable = ['tenant_id','code','name','category_id','attributes','description','is_active'];
|
||||
use SoftDeletes, BelongsToTenant, ModelTrait;
|
||||
|
||||
public function category() {
|
||||
return $this->belongsTo(CommonCode::class, 'category_id');
|
||||
}
|
||||
public function boms() {
|
||||
return $this->hasMany(Bom::class);
|
||||
}
|
||||
protected $fillable = [
|
||||
'tenant_id','code','name','category_id',
|
||||
'product_type', // 라벨/분류용
|
||||
'attributes','description','is_active',
|
||||
'is_sellable','is_purchasable','is_producible',
|
||||
'created_by','updated_by'
|
||||
];
|
||||
|
||||
// 파일 목록 (N:M, 폴리모픽)
|
||||
public function files()
|
||||
protected $casts = [
|
||||
'attributes' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
'is_sellable' => 'boolean',
|
||||
'is_purchasable' => 'boolean',
|
||||
'is_producible' => 'boolean',
|
||||
];
|
||||
|
||||
// 분류
|
||||
public function category() { return $this->belongsTo(Category::class, 'category_id'); }
|
||||
|
||||
// BOM (자기참조) — 라인 모델 경유
|
||||
public function componentLines() { return $this->hasMany(ProductComponent::class, 'parent_product_id'); } // 라인들
|
||||
public function parentLines() { return $this->hasMany(ProductComponent::class, 'child_product_id'); } // 나를 쓰는 상위 라인들
|
||||
|
||||
// 편의: 직접 children/parents 제품에 접근
|
||||
public function children()
|
||||
{
|
||||
return $this->morphMany(File::class, 'fileable');
|
||||
return $this->belongsToMany(
|
||||
self::class, 'product_components', 'parent_product_id', 'child_product_id'
|
||||
)->withPivot(['quantity','sort_order','is_default'])
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
// 태그 목록 (N:M, 폴리모픽)
|
||||
public function tags()
|
||||
public function parents()
|
||||
{
|
||||
return $this->morphToMany(Tag::class, 'taggable');
|
||||
return $this->belongsToMany(
|
||||
self::class, 'product_components', 'child_product_id', 'parent_product_id'
|
||||
)->withPivot(['quantity','sort_order','is_default'])
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
// 파일 / 태그 (폴리모픽)
|
||||
public function files() { return $this->morphMany(File::class, 'fileable'); }
|
||||
public function tags() { return $this->morphToMany(Tag::class, 'taggable'); }
|
||||
|
||||
// 스코프
|
||||
public function scopeType($q, string $type) { return $q->where('product_type', $type); }
|
||||
public function scopeSellable($q) { return $q->where('is_sellable', 1); }
|
||||
public function scopePurchasable($q) { return $q->where('is_purchasable', 1); }
|
||||
public function scopeProducible($q) { return $q->where('is_producible', 1); }
|
||||
}
|
||||
|
||||
30
app/Models/Products/ProductComponent.php
Normal file
30
app/Models/Products/ProductComponent.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Products;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class ProductComponent extends Model
|
||||
{
|
||||
use SoftDeletes, BelongsToTenant, ModelTrait;
|
||||
|
||||
protected $table = 'product_components';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id','parent_product_id','child_product_id',
|
||||
'quantity','sort_order','is_default',
|
||||
'created_by','updated_by'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quantity' => 'decimal:4',
|
||||
'sort_order' => 'integer',
|
||||
'is_default' => 'boolean',
|
||||
];
|
||||
|
||||
public function parent() { return $this->belongsTo(Product::class, 'parent_product_id'); }
|
||||
public function child() { return $this->belongsTo(Product::class, 'child_product_id'); }
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
<?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
|
||||
{
|
||||
/** 유니크 인덱스가 정확히 $columns 조합이면 드롭 */
|
||||
private function dropUniqueIfColumns(string $table, array $columns): void
|
||||
{
|
||||
$indexes = collect(DB::select("SHOW INDEX FROM `{$table}`"))->groupBy('Key_name');
|
||||
foreach ($indexes as $name => $rows) {
|
||||
$nonUnique = (int)($rows->first()->Non_unique ?? 1);
|
||||
if ($nonUnique !== 0) continue;
|
||||
$cols = collect($rows)->sortBy('Seq_in_index')->pluck('Column_name')->values()->all();
|
||||
if ($cols === $columns) {
|
||||
DB::statement("ALTER TABLE `{$table}` DROP INDEX `{$name}`");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** products.category_id 의 기존 FK 이름을 찾아 드롭 */
|
||||
private function dropProductsCategoryFk(): void
|
||||
{
|
||||
$row = DB::selectOne("
|
||||
SELECT CONSTRAINT_NAME
|
||||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'products'
|
||||
AND COLUMN_NAME = 'category_id'
|
||||
AND REFERENCED_TABLE_NAME IS NOT NULL
|
||||
LIMIT 1
|
||||
");
|
||||
if ($row && isset($row->CONSTRAINT_NAME)) {
|
||||
$name = $row->CONSTRAINT_NAME;
|
||||
DB::statement("ALTER TABLE `products` DROP FOREIGN KEY `{$name}`");
|
||||
}
|
||||
}
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
/**
|
||||
* A) categories 정리
|
||||
* - is_active: TINYINT(1) 통일 (Y/N → 1/0 안전 변환)
|
||||
* - 유니크키: (tenant_id, code_group, code) 표준화
|
||||
* - profile_code 컬럼 추가(+ 인덱스)
|
||||
*/
|
||||
// A-1. 값 변환(Y/N → 1/0) 시도 (이미 숫자여도 무해)
|
||||
try { DB::statement("UPDATE categories SET is_active = 1 WHERE is_active = 'Y'"); } catch (\Throwable $e) {}
|
||||
try { DB::statement("UPDATE categories SET is_active = 0 WHERE is_active = 'N'"); } catch (\Throwable $e) {}
|
||||
|
||||
// A-2. 타입 변경
|
||||
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)
|
||||
$this->dropUniqueIfColumns('categories', ['tenant_id', 'code']);
|
||||
$idx = collect(DB::select("SHOW INDEX FROM `categories`"))->groupBy('Key_name');
|
||||
$hasTarget = false;
|
||||
foreach ($idx as $name => $rows) {
|
||||
$nonUnique = (int)($rows->first()->Non_unique ?? 1);
|
||||
$cols = collect($rows)->sortBy('Seq_in_index')->pluck('Column_name')->values()->all();
|
||||
if ($nonUnique === 0 && $cols === ['tenant_id', 'code_group', 'code']) { $hasTarget = true; break; }
|
||||
}
|
||||
if (!$hasTarget) {
|
||||
DB::statement("ALTER TABLE `categories`
|
||||
ADD UNIQUE KEY `uq_tenant_codegroup_code` (`tenant_id`,`code_group`,`code`)");
|
||||
}
|
||||
|
||||
// A-4. 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)
|
||||
->nullable()
|
||||
->after('code_group')
|
||||
->comment("역할 프로필 코드 (common_codes.code, code_group='capability_profile')");
|
||||
}
|
||||
});
|
||||
// 인덱스 (tenant_id, profile_code)
|
||||
$hasIdx = collect(DB::select("SHOW INDEX FROM `categories`"))
|
||||
->contains(fn($r) => $r->Key_name === 'idx_categories_tenant_profilecode');
|
||||
if (!$hasIdx) {
|
||||
DB::statement("CREATE INDEX `idx_categories_tenant_profilecode` ON `categories` (`tenant_id`, `profile_code`)");
|
||||
}
|
||||
|
||||
/**
|
||||
* B) products 정리
|
||||
* - category_id FK: common_codes → categories 로 전환
|
||||
* - 역할/라벨 컬럼 추가: product_type + is_sellable/is_purchasable/is_producible
|
||||
*/
|
||||
$this->dropProductsCategoryFk();
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->foreign('category_id', 'products_category_id_foreign')
|
||||
->references('id')->on('categories')
|
||||
->onUpdate('cascade')->onDelete('restrict');
|
||||
|
||||
if (!Schema::hasColumn('products', 'product_type')) {
|
||||
$table->string('product_type', 30)
|
||||
->default('PRODUCT')
|
||||
->comment("제품유형: PRODUCT/PART/SUBASSEMBLY 등 (common_codes.code_group='product_type')")
|
||||
->after('category_id');
|
||||
}
|
||||
if (!Schema::hasColumn('products', 'is_sellable')) {
|
||||
$table->tinyInteger('is_sellable')->default(1)->comment('판매가능(1/0)')->after('description');
|
||||
}
|
||||
if (!Schema::hasColumn('products', 'is_purchasable')) {
|
||||
$table->tinyInteger('is_purchasable')->default(0)->comment('구매가능(1/0)')->after('is_sellable');
|
||||
}
|
||||
if (!Schema::hasColumn('products', 'is_producible')) {
|
||||
$table->tinyInteger('is_producible')->default(1)->comment('제조가능(1/0)')->after('is_purchasable');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* C) BOM 자기참조 테이블 신설 (product_components)
|
||||
* - parts/boms/bom_items 레거시 제거
|
||||
*/
|
||||
if (!Schema::hasTable('product_components')) {
|
||||
Schema::create('product_components', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
$table->unsignedBigInteger('parent_product_id')->comment('상위 제품 ID');
|
||||
$table->unsignedBigInteger('child_product_id')->comment('하위 제품/부품 ID');
|
||||
$table->decimal('quantity', 18, 4)->default(1.0000);
|
||||
$table->integer('sort_order')->default(0);
|
||||
$table->tinyInteger('is_default')->default(0)->comment('기본 BOM 여부(1/0)');
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->unsignedBigInteger('updated_by')->nullable();
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['tenant_id', 'parent_product_id']);
|
||||
$table->index(['tenant_id', 'child_product_id']);
|
||||
$table->unique(['tenant_id', 'parent_product_id', 'child_product_id', 'sort_order'], 'uq_component_row');
|
||||
|
||||
$table->foreign('parent_product_id')->references('id')->on('products')->onDelete('cascade');
|
||||
$table->foreign('child_product_id')->references('id')->on('products')->onDelete('restrict');
|
||||
});
|
||||
}
|
||||
|
||||
foreach (['bom_items', 'boms', 'parts'] as $tbl) {
|
||||
if (Schema::hasTable($tbl)) {
|
||||
Schema::drop($tbl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
/**
|
||||
* C) product_components 제거 + 레거시 복구(최소 스키마)
|
||||
*/
|
||||
if (Schema::hasTable('product_components')) {
|
||||
Schema::drop('product_components');
|
||||
}
|
||||
if (!Schema::hasTable('parts')) {
|
||||
Schema::create('parts', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->string('code', 30);
|
||||
$table->string('name', 100);
|
||||
$table->unsignedBigInteger('category_id');
|
||||
$table->unsignedBigInteger('part_type_id')->nullable();
|
||||
$table->string('unit', 20)->nullable();
|
||||
$table->json('attributes')->nullable();
|
||||
$table->string('description', 255)->nullable();
|
||||
$table->tinyInteger('is_active')->default(1);
|
||||
$table->unsignedBigInteger('created_by');
|
||||
$table->unsignedBigInteger('updated_by')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
$table->unique(['tenant_id','code']);
|
||||
});
|
||||
}
|
||||
if (!Schema::hasTable('boms')) {
|
||||
Schema::create('boms', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->unsignedBigInteger('product_id');
|
||||
$table->string('code', 30);
|
||||
$table->string('name', 100);
|
||||
$table->unsignedBigInteger('category_id');
|
||||
$table->json('attributes')->nullable();
|
||||
$table->string('description', 255)->nullable();
|
||||
$table->tinyInteger('is_default')->default(0);
|
||||
$table->tinyInteger('is_active')->default(1);
|
||||
$table->unsignedBigInteger('image_file_id')->nullable();
|
||||
$table->unsignedBigInteger('created_by');
|
||||
$table->unsignedBigInteger('updated_by')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
$table->unique(['tenant_id','product_id','code']);
|
||||
});
|
||||
}
|
||||
if (!Schema::hasTable('bom_items')) {
|
||||
Schema::create('bom_items', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->unsignedBigInteger('bom_id');
|
||||
$table->unsignedBigInteger('parent_id')->nullable();
|
||||
$table->string('item_type', 20);
|
||||
$table->unsignedBigInteger('ref_id');
|
||||
$table->decimal('quantity', 18, 4)->default(1.0000);
|
||||
$table->json('attributes')->nullable();
|
||||
$table->integer('sort_order')->default(0);
|
||||
$table->unsignedBigInteger('created_by');
|
||||
$table->unsignedBigInteger('updated_by')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* B) products 롤백
|
||||
* - 추가 컬럼 제거
|
||||
* - FK: categories → common_codes
|
||||
*/
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('products', 'is_sellable')) $table->dropColumn('is_sellable');
|
||||
if (Schema::hasColumn('products', 'is_purchasable')) $table->dropColumn('is_purchasable');
|
||||
if (Schema::hasColumn('products', 'is_producible')) $table->dropColumn('is_producible');
|
||||
if (Schema::hasColumn('products', 'product_type')) $table->dropColumn('product_type');
|
||||
});
|
||||
|
||||
$this->dropProductsCategoryFk();
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->foreign('category_id', 'products_category_id_foreign')
|
||||
->references('id')->on('common_codes')
|
||||
->onUpdate('cascade')->onDelete('restrict');
|
||||
});
|
||||
|
||||
/**
|
||||
* A) categories 롤백
|
||||
* - profile_code 인덱스/컬럼 제거
|
||||
* - 유니크: (tenant_id, code_group, code) → (tenant_id, code)
|
||||
* - is_active: CHAR('Y'/'N') 복구 + 값 재변환
|
||||
*/
|
||||
$hasIdx = collect(DB::select("SHOW INDEX FROM `categories`"))
|
||||
->contains(fn($r) => $r->Key_name === 'idx_categories_tenant_profilecode');
|
||||
if ($hasIdx) {
|
||||
DB::statement("DROP INDEX `idx_categories_tenant_profilecode` ON `categories`");
|
||||
}
|
||||
Schema::table('categories', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('categories', 'profile_code')) {
|
||||
$table->dropColumn('profile_code');
|
||||
}
|
||||
});
|
||||
|
||||
$idx = collect(DB::select("SHOW INDEX FROM `categories`"))->groupBy('Key_name');
|
||||
if ($idx->has('uq_tenant_codegroup_code')) {
|
||||
DB::statement("ALTER TABLE `categories` DROP INDEX `uq_tenant_codegroup_code`");
|
||||
}
|
||||
// (tenant_id, code) 유니크 복구(없을 때만)
|
||||
$idx = collect(DB::select("SHOW INDEX FROM `categories`"))->groupBy('Key_name');
|
||||
$hasOld = false;
|
||||
foreach ($idx as $name => $rows) {
|
||||
$nonUnique = (int)($rows->first()->Non_unique ?? 1);
|
||||
$cols = collect($rows)->sortBy('Seq_in_index')->pluck('Column_name')->values()->all();
|
||||
if ($nonUnique === 0 && $cols === ['tenant_id', 'code']) { $hasOld = true; break; }
|
||||
}
|
||||
if (!$hasOld) {
|
||||
DB::statement("ALTER TABLE `categories`
|
||||
ADD UNIQUE KEY `categories_tenant_id_code_unique` (`tenant_id`,`code`)");
|
||||
}
|
||||
|
||||
// 타입 복구 + 값 재변환
|
||||
DB::statement("ALTER TABLE categories
|
||||
MODIFY COLUMN is_active CHAR(1) NOT NULL DEFAULT 'Y' COMMENT '활성여부(Y/N)'");
|
||||
try { DB::statement("UPDATE categories SET is_active = 'Y' WHERE is_active = 1"); } catch (\Throwable $e) {}
|
||||
try { DB::statement("UPDATE categories SET is_active = 'N' WHERE is_active = 0"); } catch (\Throwable $e) {}
|
||||
}
|
||||
};
|
||||
75
database/seeders/CapabilityProfileSeeder.php
Normal file
75
database/seeders/CapabilityProfileSeeder.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CapabilityProfileSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$tenantId = null; // null = 공통, 필요시 테넌트별로도 생성 가능
|
||||
$group = 'capability_profile';
|
||||
|
||||
$profiles = [
|
||||
[
|
||||
'code' => 'FINISHED_GOOD',
|
||||
'name' => '완제품',
|
||||
'attributes' => [
|
||||
'is_sellable' => 1,
|
||||
'is_purchasable' => 0,
|
||||
'is_producible' => 1,
|
||||
'is_stock_managed' => 1,
|
||||
],
|
||||
],
|
||||
[
|
||||
'code' => 'SUB_ASSEMBLY',
|
||||
'name' => '서브어셈블리',
|
||||
'attributes' => [
|
||||
'is_sellable' => 0,
|
||||
'is_purchasable' => 0,
|
||||
'is_producible' => 1,
|
||||
'is_stock_managed' => 1,
|
||||
],
|
||||
],
|
||||
[
|
||||
'code' => 'PURCHASED_PART',
|
||||
'name' => '구매부품',
|
||||
'attributes' => [
|
||||
'is_sellable' => 0,
|
||||
'is_purchasable' => 1,
|
||||
'is_producible' => 0,
|
||||
'is_stock_managed' => 1,
|
||||
],
|
||||
],
|
||||
[
|
||||
'code' => 'PHANTOM',
|
||||
'name' => '팬텀(가상)',
|
||||
'attributes' => [
|
||||
'is_sellable' => 0,
|
||||
'is_purchasable' => 0,
|
||||
'is_producible' => 1,
|
||||
'is_stock_managed' => 0,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($profiles as $p) {
|
||||
DB::table('common_codes')->updateOrInsert(
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'code_group' => $group,
|
||||
'code' => $p['code'],
|
||||
],
|
||||
[
|
||||
'name' => $p['name'],
|
||||
'attributes' => json_encode($p['attributes']),
|
||||
'description' => '기본 프로필',
|
||||
'is_active' => 1,
|
||||
'sort_order' => 0,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
database/seeders/CategorySeeder.php
Normal file
24
database/seeders/CategorySeeder.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use App\Models\Commons\Category;
|
||||
|
||||
class CategorySeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
Category::create([
|
||||
'tenant_id' => 1,
|
||||
'parent_id' => null,
|
||||
'code_group' => 'product',
|
||||
'code' => 'DEFAULT',
|
||||
'name' => '기본 카테고리',
|
||||
'profile_code'=> 'FINISHED_GOOD', // capability_profile 참조
|
||||
'is_active' => 1,
|
||||
'sort_order' => 1,
|
||||
'description' => '시스템 기본 카테고리',
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user