feat: 5130 레거시 데이터 마이그레이션 커맨드 추가
- config/database.php: chandj (5130 레거시 DB) 연결 설정 추가 - Migrate5130Items 커맨드: 5130 품목 데이터를 SAM items 테이블로 마이그레이션 - models, parts, parts_sub, bdmodels 단계별 마이그레이션 지원 - dry-run 모드로 시뮬레이션 가능 - rollback 옵션으로 마이그레이션 취소 가능 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
652
app/Console/Commands/Migrate5130Items.php
Normal file
652
app/Console/Commands/Migrate5130Items.php
Normal file
@@ -0,0 +1,652 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Attributes\AsCommand;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
#[AsCommand(name: 'migrate:5130-items', description: '5130 품목 데이터를 SAM items 테이블로 마이그레이션')]
|
||||||
|
class Migrate5130Items extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'migrate:5130-items
|
||||||
|
{--tenant_id=287 : Target tenant ID (default: 287 경동기업)}
|
||||||
|
{--dry-run : 실제 저장 없이 시뮬레이션만 수행}
|
||||||
|
{--step=all : 실행할 단계 (all|models|parts|parts_sub|bdmodels|relations)}
|
||||||
|
{--rollback : 마이그레이션 롤백 (source=5130 데이터 삭제)}';
|
||||||
|
|
||||||
|
// 5130 DB 연결
|
||||||
|
private string $sourceDb = 'chandj';
|
||||||
|
|
||||||
|
// SAM DB 연결
|
||||||
|
private string $targetDb = 'samdb';
|
||||||
|
|
||||||
|
// 매핑 캐시 (source_table:source_id => item_id)
|
||||||
|
private array $idMappings = [];
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$tenantId = (int) $this->option('tenant_id');
|
||||||
|
$dryRun = $this->option('dry-run');
|
||||||
|
$step = $this->option('step');
|
||||||
|
$rollback = $this->option('rollback');
|
||||||
|
|
||||||
|
$this->info("=== 5130 → SAM 품목 마이그레이션 ===");
|
||||||
|
$this->info("Tenant ID: {$tenantId}");
|
||||||
|
$this->info("Mode: " . ($dryRun ? 'DRY-RUN (시뮬레이션)' : 'LIVE'));
|
||||||
|
$this->info("Step: {$step}");
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
if ($rollback) {
|
||||||
|
return $this->rollbackMigration($tenantId, $dryRun);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기존 매핑 로드
|
||||||
|
$this->loadExistingMappings();
|
||||||
|
|
||||||
|
$steps = $step === 'all'
|
||||||
|
? ['models', 'parts', 'parts_sub', 'bdmodels', 'relations']
|
||||||
|
: [$step];
|
||||||
|
|
||||||
|
foreach ($steps as $currentStep) {
|
||||||
|
$this->line(">>> Step: {$currentStep}");
|
||||||
|
|
||||||
|
match ($currentStep) {
|
||||||
|
'models' => $this->migrateModels($tenantId, $dryRun),
|
||||||
|
'parts' => $this->migrateParts($tenantId, $dryRun),
|
||||||
|
'parts_sub' => $this->migratePartsSub($tenantId, $dryRun),
|
||||||
|
'bdmodels' => $this->migrateBDModels($tenantId, $dryRun),
|
||||||
|
'relations' => $this->migrateRelations($tenantId, $dryRun),
|
||||||
|
default => $this->error("Unknown step: {$currentStep}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("=== 마이그레이션 완료 ===");
|
||||||
|
$this->showSummary();
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기존 매핑 로드 (중복 방지)
|
||||||
|
*/
|
||||||
|
private function loadExistingMappings(): void
|
||||||
|
{
|
||||||
|
$mappings = DB::connection('mysql')->table('item_id_mappings')->get();
|
||||||
|
|
||||||
|
foreach ($mappings as $mapping) {
|
||||||
|
$key = "{$mapping->source_table}:{$mapping->source_id}";
|
||||||
|
$this->idMappings[$key] = $mapping->item_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->line("Loaded " . count($this->idMappings) . " existing mappings");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* models → items (FG)
|
||||||
|
*/
|
||||||
|
private function migrateModels(int $tenantId, bool $dryRun): void
|
||||||
|
{
|
||||||
|
$this->info("Migrating models → items (FG)...");
|
||||||
|
|
||||||
|
$models = DB::connection('chandj')->table('models')
|
||||||
|
->where('is_deleted', 0)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$this->line("Found {$models->count()} models");
|
||||||
|
|
||||||
|
$bar = $this->output->createProgressBar($models->count());
|
||||||
|
$bar->start();
|
||||||
|
|
||||||
|
foreach ($models as $model) {
|
||||||
|
$mappingKey = "models:{$model->model_id}";
|
||||||
|
|
||||||
|
// 이미 마이그레이션된 경우 스킵
|
||||||
|
if (isset($this->idMappings[$mappingKey])) {
|
||||||
|
$bar->advance();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$itemData = [
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'item_type' => 'FG',
|
||||||
|
'code' => $model->model_name,
|
||||||
|
'name' => $model->model_name,
|
||||||
|
'unit' => 'SET',
|
||||||
|
'description' => $model->description,
|
||||||
|
'attributes' => json_encode([
|
||||||
|
'major_category' => $model->major_category,
|
||||||
|
'finishing_type' => $model->finishing_type,
|
||||||
|
'guiderail_type' => $model->guiderail_type,
|
||||||
|
'source' => '5130',
|
||||||
|
'source_table' => 'models',
|
||||||
|
'source_id' => $model->model_id,
|
||||||
|
]),
|
||||||
|
'is_active' => 1,
|
||||||
|
'created_at' => $model->created_at ?? now(),
|
||||||
|
'updated_at' => $model->updated_at ?? now(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!$dryRun) {
|
||||||
|
$itemId = DB::connection('mysql')->table('items')->insertGetId($itemData);
|
||||||
|
|
||||||
|
// 매핑 기록
|
||||||
|
DB::connection('mysql')->table('item_id_mappings')->insert([
|
||||||
|
'source_table' => 'models',
|
||||||
|
'source_id' => $model->model_id,
|
||||||
|
'item_id' => $itemId,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->idMappings[$mappingKey] = $itemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("Models migration completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* parts → items (PT)
|
||||||
|
*/
|
||||||
|
private function migrateParts(int $tenantId, bool $dryRun): void
|
||||||
|
{
|
||||||
|
$this->info("Migrating parts → items (PT)...");
|
||||||
|
|
||||||
|
$parts = DB::connection('chandj')->table('parts')
|
||||||
|
->where('is_deleted', 0)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$this->line("Found {$parts->count()} parts");
|
||||||
|
|
||||||
|
$bar = $this->output->createProgressBar($parts->count());
|
||||||
|
$bar->start();
|
||||||
|
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$mappingKey = "parts:{$part->part_id}";
|
||||||
|
|
||||||
|
if (isset($this->idMappings[$mappingKey])) {
|
||||||
|
$bar->advance();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = $this->generateCode('PT', $part->part_name);
|
||||||
|
|
||||||
|
$itemData = [
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'item_type' => 'PT',
|
||||||
|
'code' => $code,
|
||||||
|
'name' => $part->part_name ?: '(이름없음)',
|
||||||
|
'unit' => $part->unit ?: 'EA',
|
||||||
|
'description' => $part->memo,
|
||||||
|
'attributes' => json_encode([
|
||||||
|
'spec' => $part->spec,
|
||||||
|
'unit_price' => $this->parseNumber($part->unitprice),
|
||||||
|
'image_url' => $part->img_url,
|
||||||
|
'source' => '5130',
|
||||||
|
'source_table' => 'parts',
|
||||||
|
'source_id' => $part->part_id,
|
||||||
|
'parent_model_id' => $part->model_id,
|
||||||
|
]),
|
||||||
|
'is_active' => 1,
|
||||||
|
'created_at' => $part->created_at ?? now(),
|
||||||
|
'updated_at' => $part->updated_at ?? now(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!$dryRun) {
|
||||||
|
$itemId = DB::connection('mysql')->table('items')->insertGetId($itemData);
|
||||||
|
|
||||||
|
DB::connection('mysql')->table('item_id_mappings')->insert([
|
||||||
|
'source_table' => 'parts',
|
||||||
|
'source_id' => $part->part_id,
|
||||||
|
'item_id' => $itemId,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->idMappings[$mappingKey] = $itemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("Parts migration completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* parts_sub → items (RM)
|
||||||
|
*/
|
||||||
|
private function migratePartsSub(int $tenantId, bool $dryRun): void
|
||||||
|
{
|
||||||
|
$this->info("Migrating parts_sub → items (RM)...");
|
||||||
|
|
||||||
|
$partsSub = DB::connection('chandj')->table('parts_sub')
|
||||||
|
->where('is_deleted', 0)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$this->line("Found {$partsSub->count()} parts_sub");
|
||||||
|
|
||||||
|
$bar = $this->output->createProgressBar($partsSub->count());
|
||||||
|
$bar->start();
|
||||||
|
|
||||||
|
foreach ($partsSub as $sub) {
|
||||||
|
$mappingKey = "parts_sub:{$sub->subpart_id}";
|
||||||
|
|
||||||
|
if (isset($this->idMappings[$mappingKey])) {
|
||||||
|
$bar->advance();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = $this->generateCode('RM', $sub->subpart_name);
|
||||||
|
|
||||||
|
$itemData = [
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'item_type' => 'RM',
|
||||||
|
'code' => $code,
|
||||||
|
'name' => $sub->subpart_name ?: '(이름없음)',
|
||||||
|
'unit' => 'EA',
|
||||||
|
'description' => null,
|
||||||
|
'attributes' => json_encode([
|
||||||
|
'material' => $sub->material,
|
||||||
|
'bend_sum' => $this->parseNumber($sub->bendSum),
|
||||||
|
'plate_sum' => $this->parseNumber($sub->plateSum),
|
||||||
|
'final_sum' => $this->parseNumber($sub->finalSum),
|
||||||
|
'unit_price' => $this->parseNumber($sub->unitPrice),
|
||||||
|
'computed_price' => $this->parseNumber($sub->computedPrice),
|
||||||
|
'line_total' => $this->parseNumber($sub->lineTotal),
|
||||||
|
'image_url' => $sub->image_url,
|
||||||
|
'source' => '5130',
|
||||||
|
'source_table' => 'parts_sub',
|
||||||
|
'source_id' => $sub->subpart_id,
|
||||||
|
'parent_part_id' => $sub->part_id,
|
||||||
|
]),
|
||||||
|
'is_active' => 1,
|
||||||
|
'created_at' => $sub->created_at ?? now(),
|
||||||
|
'updated_at' => $sub->updated_at ?? now(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!$dryRun) {
|
||||||
|
$itemId = DB::connection('mysql')->table('items')->insertGetId($itemData);
|
||||||
|
|
||||||
|
DB::connection('mysql')->table('item_id_mappings')->insert([
|
||||||
|
'source_table' => 'parts_sub',
|
||||||
|
'source_id' => $sub->subpart_id,
|
||||||
|
'item_id' => $itemId,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->idMappings[$mappingKey] = $itemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("Parts_sub migration completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BDmodels → items (PT) + savejson → items (RM) + 관계
|
||||||
|
*/
|
||||||
|
private function migrateBDModels(int $tenantId, bool $dryRun): void
|
||||||
|
{
|
||||||
|
$this->info("Migrating BDmodels → items (PT + RM with BOM)...");
|
||||||
|
|
||||||
|
$bdmodels = DB::connection('chandj')->table('BDmodels')
|
||||||
|
->where('is_deleted', 0)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$this->line("Found {$bdmodels->count()} BDmodels");
|
||||||
|
|
||||||
|
$bar = $this->output->createProgressBar($bdmodels->count());
|
||||||
|
$bar->start();
|
||||||
|
|
||||||
|
$bomCount = 0;
|
||||||
|
|
||||||
|
foreach ($bdmodels as $bd) {
|
||||||
|
$mappingKey = "BDmodels:{$bd->num}";
|
||||||
|
|
||||||
|
// 이미 마이그레이션된 경우 스킵
|
||||||
|
if (isset($this->idMappings[$mappingKey])) {
|
||||||
|
$bar->advance();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BDmodels → items (PT: 부품)
|
||||||
|
$code = $this->generateCode('BD', $bd->seconditem ?: $bd->model_name ?: "BD{$bd->num}");
|
||||||
|
|
||||||
|
$itemData = [
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'item_type' => 'PT',
|
||||||
|
'code' => $code,
|
||||||
|
'name' => $bd->seconditem ?: $bd->model_name ?: "(이름없음)",
|
||||||
|
'unit' => 'EA',
|
||||||
|
'description' => $bd->description,
|
||||||
|
'attributes' => json_encode([
|
||||||
|
'model_name' => $bd->model_name,
|
||||||
|
'major_category' => $bd->major_category,
|
||||||
|
'spec' => $bd->spec,
|
||||||
|
'finishing_type' => $bd->finishing_type,
|
||||||
|
'check_type' => $bd->check_type,
|
||||||
|
'exit_direction' => $bd->exit_direction,
|
||||||
|
'unit_price' => $this->parseNumber($bd->unitprice),
|
||||||
|
'price_date' => $bd->priceDate,
|
||||||
|
'source' => '5130',
|
||||||
|
'source_table' => 'BDmodels',
|
||||||
|
'source_id' => $bd->num,
|
||||||
|
]),
|
||||||
|
'is_active' => 1,
|
||||||
|
'created_at' => $bd->created_at ?? now(),
|
||||||
|
'updated_at' => $bd->updated_at ?? now(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$parentItemId = null;
|
||||||
|
|
||||||
|
if (!$dryRun) {
|
||||||
|
$parentItemId = DB::connection('mysql')->table('items')->insertGetId($itemData);
|
||||||
|
|
||||||
|
DB::connection('mysql')->table('item_id_mappings')->insert([
|
||||||
|
'source_table' => 'BDmodels',
|
||||||
|
'source_id' => $bd->num,
|
||||||
|
'item_id' => $parentItemId,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->idMappings[$mappingKey] = $parentItemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// savejson → 자식 items (RM: 원자재) + 관계
|
||||||
|
if (!empty($bd->savejson)) {
|
||||||
|
$bomItems = json_decode($bd->savejson, true);
|
||||||
|
|
||||||
|
if (is_array($bomItems)) {
|
||||||
|
$orderNo = 0;
|
||||||
|
foreach ($bomItems as $bomItem) {
|
||||||
|
$orderNo++;
|
||||||
|
$childMappingKey = "BDmodels_bom:{$bd->num}:{$orderNo}";
|
||||||
|
|
||||||
|
if (isset($this->idMappings[$childMappingKey])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// col1: 품명, col2: 재질, col3: 여유, col4: 전개, col5: 합계
|
||||||
|
// col6: 단가, col7: 금액, col8: 수량, col9: 총액, col10: 비고
|
||||||
|
$childName = $bomItem['col1'] ?? "(BOM항목)";
|
||||||
|
$childCode = $this->generateCode('RM', $childName . "_" . $bd->num . "_" . $orderNo);
|
||||||
|
|
||||||
|
$childData = [
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'item_type' => 'RM',
|
||||||
|
'code' => $childCode,
|
||||||
|
'name' => $childName,
|
||||||
|
'unit' => 'EA',
|
||||||
|
'description' => $bomItem['col10'] ?? null,
|
||||||
|
'attributes' => json_encode([
|
||||||
|
'material' => $bomItem['col2'] ?? null,
|
||||||
|
'margin' => $this->parseNumber($bomItem['col3'] ?? null),
|
||||||
|
'unfold' => $this->parseNumber($bomItem['col4'] ?? null),
|
||||||
|
'total_length' => $this->parseNumber($bomItem['col5'] ?? null),
|
||||||
|
'unit_price' => $this->parseNumber($bomItem['col6'] ?? null),
|
||||||
|
'amount' => $this->parseNumber($bomItem['col7'] ?? null),
|
||||||
|
'quantity' => $this->parseNumber($bomItem['col8'] ?? null),
|
||||||
|
'line_total' => $this->parseNumber($bomItem['col9'] ?? null),
|
||||||
|
'source' => '5130',
|
||||||
|
'source_table' => 'BDmodels_bom',
|
||||||
|
'source_id' => "{$bd->num}:{$orderNo}",
|
||||||
|
'parent_bdmodel_id' => $bd->num,
|
||||||
|
]),
|
||||||
|
'is_active' => 1,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!$dryRun) {
|
||||||
|
$childItemId = DB::connection('mysql')->table('items')->insertGetId($childData);
|
||||||
|
|
||||||
|
// BOM 항목은 item_id_mappings에 저장하지 않음
|
||||||
|
// (source_id가 bigint이라 "17:1" 같은 복합키 저장 불가)
|
||||||
|
// 대신 attributes에 source 정보가 있고 entity_relationships로 관계 추적
|
||||||
|
|
||||||
|
$this->idMappings[$childMappingKey] = $childItemId;
|
||||||
|
|
||||||
|
// 관계 생성 (BDmodel → BOM item)
|
||||||
|
if ($parentItemId) {
|
||||||
|
DB::connection('mysql')->table('entity_relationships')->insert([
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'group_id' => 1,
|
||||||
|
'parent_type' => 'items',
|
||||||
|
'parent_id' => $parentItemId,
|
||||||
|
'child_type' => 'items',
|
||||||
|
'child_id' => $childItemId,
|
||||||
|
'order_no' => $orderNo,
|
||||||
|
'metadata' => json_encode([
|
||||||
|
'quantity' => $this->parseNumber($bomItem['col8'] ?? '1'),
|
||||||
|
'relation' => 'bom',
|
||||||
|
'source' => '5130',
|
||||||
|
]),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$bomCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("BDmodels migration completed (BOM items: {$bomCount})");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 관계 마이그레이션 (entity_relationships)
|
||||||
|
*/
|
||||||
|
private function migrateRelations(int $tenantId, bool $dryRun): void
|
||||||
|
{
|
||||||
|
$this->info("Migrating relations → entity_relationships...");
|
||||||
|
|
||||||
|
// 1. models ↔ parts 관계
|
||||||
|
$this->info(" → models ↔ parts relations...");
|
||||||
|
$parts = DB::connection('chandj')->table('parts')
|
||||||
|
->where('is_deleted', 0)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$relCount = 0;
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$parentKey = "models:{$part->model_id}";
|
||||||
|
$childKey = "parts:{$part->part_id}";
|
||||||
|
|
||||||
|
if (!isset($this->idMappings[$parentKey]) || !isset($this->idMappings[$childKey])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$exists = DB::connection('mysql')->table('entity_relationships')
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->where('parent_type', 'items')
|
||||||
|
->where('parent_id', $this->idMappings[$parentKey])
|
||||||
|
->where('child_type', 'items')
|
||||||
|
->where('child_id', $this->idMappings[$childKey])
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (!$exists && !$dryRun) {
|
||||||
|
DB::connection('mysql')->table('entity_relationships')->insert([
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'group_id' => 1,
|
||||||
|
'parent_type' => 'items',
|
||||||
|
'parent_id' => $this->idMappings[$parentKey],
|
||||||
|
'child_type' => 'items',
|
||||||
|
'child_id' => $this->idMappings[$childKey],
|
||||||
|
'order_no' => $part->part_id,
|
||||||
|
'metadata' => json_encode([
|
||||||
|
'quantity' => $part->quantity,
|
||||||
|
'relation' => 'bom',
|
||||||
|
'source' => '5130',
|
||||||
|
]),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
$relCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->line(" Created {$relCount} model-part relations");
|
||||||
|
|
||||||
|
// 2. parts ↔ parts_sub 관계
|
||||||
|
$this->info(" → parts ↔ parts_sub relations...");
|
||||||
|
$partsSub = DB::connection('chandj')->table('parts_sub')
|
||||||
|
->where('is_deleted', 0)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$relCount = 0;
|
||||||
|
foreach ($partsSub as $sub) {
|
||||||
|
$parentKey = "parts:{$sub->part_id}";
|
||||||
|
$childKey = "parts_sub:{$sub->subpart_id}";
|
||||||
|
|
||||||
|
if (!isset($this->idMappings[$parentKey]) || !isset($this->idMappings[$childKey])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$exists = DB::connection('mysql')->table('entity_relationships')
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->where('parent_type', 'items')
|
||||||
|
->where('parent_id', $this->idMappings[$parentKey])
|
||||||
|
->where('child_type', 'items')
|
||||||
|
->where('child_id', $this->idMappings[$childKey])
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (!$exists && !$dryRun) {
|
||||||
|
DB::connection('mysql')->table('entity_relationships')->insert([
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'group_id' => 1,
|
||||||
|
'parent_type' => 'items',
|
||||||
|
'parent_id' => $this->idMappings[$parentKey],
|
||||||
|
'child_type' => 'items',
|
||||||
|
'child_id' => $this->idMappings[$childKey],
|
||||||
|
'order_no' => $sub->subpart_id,
|
||||||
|
'metadata' => json_encode([
|
||||||
|
'quantity' => $sub->quantity,
|
||||||
|
'relation' => 'bom',
|
||||||
|
'source' => '5130',
|
||||||
|
]),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
$relCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->line(" Created {$relCount} part-subpart relations");
|
||||||
|
|
||||||
|
$this->info("Relations migration completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 롤백 (source=5130 데이터 삭제)
|
||||||
|
*/
|
||||||
|
private function rollbackMigration(int $tenantId, bool $dryRun): int
|
||||||
|
{
|
||||||
|
$this->warn("=== 롤백 모드 ===");
|
||||||
|
|
||||||
|
if (!$this->confirm('5130에서 마이그레이션된 모든 데이터를 삭제하시겠습니까?')) {
|
||||||
|
$this->info('롤백 취소됨');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. entity_relationships 삭제
|
||||||
|
$relCount = DB::connection('mysql')->table('entity_relationships')
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->whereRaw("JSON_EXTRACT(metadata, '$.source') = '5130'")
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if (!$dryRun) {
|
||||||
|
DB::connection('mysql')->table('entity_relationships')
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->whereRaw("JSON_EXTRACT(metadata, '$.source') = '5130'")
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
$this->line("Deleted {$relCount} entity_relationships");
|
||||||
|
|
||||||
|
// 2. items 삭제 (매핑 기반)
|
||||||
|
$mappings = DB::connection('mysql')->table('item_id_mappings')->get();
|
||||||
|
$itemIds = $mappings->pluck('item_id')->toArray();
|
||||||
|
|
||||||
|
if (!$dryRun && !empty($itemIds)) {
|
||||||
|
DB::connection('mysql')->table('items')
|
||||||
|
->whereIn('id', $itemIds)
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
$this->line("Deleted " . count($itemIds) . " items");
|
||||||
|
|
||||||
|
// 3. item_id_mappings 삭제
|
||||||
|
if (!$dryRun) {
|
||||||
|
DB::connection('mysql')->table('item_id_mappings')->truncate();
|
||||||
|
}
|
||||||
|
$this->line("Cleared item_id_mappings");
|
||||||
|
|
||||||
|
$this->info("롤백 완료");
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 코드 생성
|
||||||
|
*/
|
||||||
|
private function generateCode(string $prefix, ?string $name): string
|
||||||
|
{
|
||||||
|
if (empty($name)) {
|
||||||
|
return $prefix . '-' . Str::random(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 한글은 유지, 특수문자 제거, 공백→언더스코어
|
||||||
|
$code = preg_replace('/[^\p{L}\p{N}\s]/u', '', $name);
|
||||||
|
$code = preg_replace('/\s+/', '_', trim($code));
|
||||||
|
$code = Str::upper($code);
|
||||||
|
|
||||||
|
return $prefix . '-' . Str::limit($code, 50, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 숫자 파싱 (varchar → decimal)
|
||||||
|
*/
|
||||||
|
private function parseNumber(?string $value): ?float
|
||||||
|
{
|
||||||
|
if (empty($value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cleaned = preg_replace('/[^\d.-]/', '', $value);
|
||||||
|
return is_numeric($cleaned) ? (float) $cleaned : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 요약 출력
|
||||||
|
*/
|
||||||
|
private function showSummary(): void
|
||||||
|
{
|
||||||
|
$this->newLine();
|
||||||
|
$this->table(
|
||||||
|
['Source Table', 'Migrated Count'],
|
||||||
|
[
|
||||||
|
['models', count(array_filter(array_keys($this->idMappings), fn($k) => str_starts_with($k, 'models:')))],
|
||||||
|
['parts', count(array_filter(array_keys($this->idMappings), fn($k) => str_starts_with($k, 'parts:') && !str_starts_with($k, 'parts_sub:')))],
|
||||||
|
['parts_sub', count(array_filter(array_keys($this->idMappings), fn($k) => str_starts_with($k, 'parts_sub:')))],
|
||||||
|
['BDmodels', count(array_filter(array_keys($this->idMappings), fn($k) => str_starts_with($k, 'BDmodels:') && !str_starts_with($k, 'BDmodels_bom:')))],
|
||||||
|
['BDmodels_bom', count(array_filter(array_keys($this->idMappings), fn($k) => str_starts_with($k, 'BDmodels_bom:')))],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,6 +62,23 @@
|
|||||||
]) : [],
|
]) : [],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// 5130 레거시 DB (chandj)
|
||||||
|
'chandj' => [
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'host' => env('CHANDJ_DB_HOST', env('DB_HOST', '127.0.0.1')),
|
||||||
|
'port' => env('CHANDJ_DB_PORT', env('DB_PORT', '3306')),
|
||||||
|
'database' => env('CHANDJ_DB_DATABASE', 'chandj'),
|
||||||
|
'username' => env('CHANDJ_DB_USERNAME', env('DB_USERNAME', 'root')),
|
||||||
|
'password' => env('CHANDJ_DB_PASSWORD', env('DB_PASSWORD', '')),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => 'utf8mb4',
|
||||||
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => false,
|
||||||
|
'engine' => null,
|
||||||
|
],
|
||||||
|
|
||||||
'mariadb' => [
|
'mariadb' => [
|
||||||
'driver' => 'mariadb',
|
'driver' => 'mariadb',
|
||||||
'url' => env('DB_URL'),
|
'url' => env('DB_URL'),
|
||||||
|
|||||||
Reference in New Issue
Block a user