option('tenant_id'); $dryRun = $this->option('dry-run'); $rollback = $this->option('rollback'); if ($rollback) { return $this->rollback($tenantId); } // 이미 이관된 데이터 확인 $existingCount = BendingItem::where('tenant_id', $tenantId)->count(); if ($existingCount > 0) { $this->warn("이미 bending_items에 {$existingCount}건 존재합니다."); if (! $this->confirm('기존 데이터 삭제 후 재이관하시겠습니까?')) { return 0; } $this->rollback($tenantId); } // items(BENDING) 조회 $items = Item::where('item_category', 'BENDING') ->where('tenant_id', $tenantId) ->get(); $this->info("이관 대상: {$items->count()}건"); if ($dryRun) { $this->previewItems($items); return 0; } $success = 0; $errors = 0; $bdCount = 0; DB::transaction(function () use ($items, $tenantId, &$success, &$errors, &$bdCount) { foreach ($items as $item) { try { $bi = $this->migrateItem($item, $tenantId); $bdRows = $this->migrateBendingData($bi, $item); $bdCount += $bdRows; $success++; } catch (\Throwable $e) { $this->error(" ❌ {$item->code}: {$e->getMessage()}"); $errors++; } } }); $this->newLine(); $this->info("완료: {$success}건 이관, {$bdCount}건 전개도 행, {$errors}건 오류"); return $errors > 0 ? 1 : 0; } private function migrateItem(Item $item, int $tenantId): BendingItem { $opts = $item->options ?? []; // item_name: options.item_name → name 폴백 $itemName = $opts['item_name'] ?? null; if (empty($itemName) || $itemName === 'null') { $itemName = $item->name; } $bi = BendingItem::create([ 'tenant_id' => $tenantId, 'code' => $item->code, 'legacy_code' => $item->code, 'legacy_bending_id' => $opts['legacy_bending_num'] ?? null, // 정규 컬럼 (options에서 승격) 'item_name' => $itemName, 'item_sep' => $this->cleanNull($opts['item_sep'] ?? null), 'item_bending' => $this->cleanNull($opts['item_bending'] ?? null), 'material' => $this->cleanNull($opts['material'] ?? null), 'item_spec' => $this->cleanNull($opts['item_spec'] ?? null), 'model_name' => $this->cleanNull($opts['model_name'] ?? null), 'model_UA' => $this->cleanNull($opts['model_UA'] ?? null), 'rail_width' => $this->toDecimal($opts['rail_width'] ?? null), 'exit_direction' => $this->cleanNull($opts['exit_direction'] ?? null), 'box_width' => $this->toDecimal($opts['box_width'] ?? null), 'box_height' => $this->toDecimal($opts['box_height'] ?? null), 'front_bottom' => $this->toDecimal($opts['front_bottom_width'] ?? $opts['front_bottom'] ?? null), 'inspection_door' => $this->cleanNull($opts['inspection_door'] ?? null), // 비정형 속성 'options' => $this->buildMetaOptions($opts), 'is_active' => $item->is_active, 'created_by' => $item->created_by, 'updated_by' => $item->updated_by, ]); $this->line(" ✅ {$item->code} → bending_items#{$bi->id} ({$itemName})"); return $bi; } private function migrateBendingData(BendingItem $bi, Item $item): int { $opts = $item->options ?? []; $bendingData = $opts['bendingData'] ?? []; if (empty($bendingData) || ! is_array($bendingData)) { return 0; } // bending_items.bending_data JSON 컬럼에 저장 $bi->update(['bending_data' => $bendingData]); return count($bendingData); } private function rollback(int $tenantId): int { $biCount = BendingItem::where('tenant_id', $tenantId)->count(); BendingItem::where('tenant_id', $tenantId)->forceDelete(); $this->info("롤백 완료: bending_items {$biCount}건 삭제"); return 0; } private function previewItems($items): void { $headers = ['code', 'name', 'item_name(opts)', 'item_sep', 'material', 'has_bd']; $rows = $items->take(20)->map(function ($item) { $opts = $item->options ?? []; return [ $item->code, mb_substr($item->name, 0, 20), mb_substr($opts['item_name'] ?? '(NULL)', 0, 20), $opts['item_sep'] ?? '-', $opts['material'] ?? '-', ! empty($opts['bendingData']) ? '✅' : '❌', ]; }); $this->table($headers, $rows); $nullNameCount = $items->filter(fn ($i) => empty(($i->options ?? [])['item_name']))->count(); $hasBdCount = $items->filter(fn ($i) => ! empty(($i->options ?? [])['bendingData']))->count(); $this->info("item_name NULL: {$nullNameCount}건 (name 필드로 폴백)"); $this->info("bendingData 있음: {$hasBdCount}건"); } private function cleanNull(?string $value): ?string { if ($value === null || $value === 'null' || $value === '') { return null; } return $value; } private function toDecimal(mixed $value): ?float { if ($value === null || $value === 'null' || $value === '') { return null; } return (float) $value; } /** * options에 남길 비정형 속성만 추출 */ private function buildMetaOptions(array $opts): ?array { $metaKeys = ['search_keyword', 'registration_date', 'author', 'memo', 'parent_num', 'modified_by']; $meta = []; foreach ($metaKeys as $key) { $val = $opts[$key] ?? null; if ($val !== null && $val !== 'null' && $val !== '') { $meta[$key] = $val; } } return empty($meta) ? null : $meta; } }