'FG-SCR-001', 'STEEL' => 'FG-STL-001', 'BENDING' => 'FG-BND-001', ]; /** * 로드된 BOM 템플릿 캐시 */ private array $bomTemplates = []; public function handle(): int { $this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); $this->info(' 5130 → SAM BOM 마이그레이션'); $this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); $this->newLine(); $dryRun = $this->option('dry-run'); $code = $this->option('code'); $category = $this->option('category'); $tenantId = (int) $this->option('tenant-id'); if ($dryRun) { $this->warn('🔍 DRY-RUN 모드: 실제 변경 없이 시뮬레이션합니다.'); $this->newLine(); } // 1. BOM 템플릿 로드 $this->info('📥 Step 1: BOM 템플릿 로드'); if (! $this->loadBomTemplates($tenantId)) { $this->error('BOM 템플릿 로드 실패'); return Command::FAILURE; } // 2. 대상 품목 조회 $this->info('📥 Step 2: 대상 품목 조회'); $items = $this->getTargetItems($tenantId, $code, $category); if ($items->isEmpty()) { $this->info('✅ 처리할 대상 품목이 없습니다.'); return Command::SUCCESS; } $this->info(" 대상 품목 수: {$items->count()}건"); $this->newLine(); // 3. BOM 적용 $this->info('🔄 Step 3: BOM 적용'); $stats = [ 'total' => $items->count(), 'success' => 0, 'skipped' => 0, 'failed' => 0, ]; $this->output->progressStart($items->count()); foreach ($items as $item) { $result = $this->applyBomToItem($item, $dryRun); if ($result === 'success') { $stats['success']++; } elseif ($result === 'skipped') { $stats['skipped']++; } else { $stats['failed']++; } $this->output->progressAdvance(); } $this->output->progressFinish(); $this->newLine(); // 4. 결과 출력 $this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); $this->info('📊 처리 결과'); $this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); $this->table( ['항목', '건수'], [ ['전체 대상', $stats['total']], ['✅ 성공', $stats['success']], ['⏭️ 스킵 (템플릿 없음)', $stats['skipped']], ['❌ 실패', $stats['failed']], ] ); if ($dryRun) { $this->newLine(); $this->warn('🔍 DRY-RUN 모드였습니다. 실제 적용하려면 --dry-run 옵션을 제거하세요.'); } return Command::SUCCESS; } /** * BOM 템플릿 로드 */ private function loadBomTemplates(int $tenantId): bool { foreach ($this->templateSources as $category => $sourceCode) { $sourceItem = DB::table('items') ->where('tenant_id', $tenantId) ->where('code', $sourceCode) ->first(['bom']); if ($sourceItem && $sourceItem->bom) { $bom = json_decode($sourceItem->bom, true); if (is_array($bom) && count($bom) > 0) { $this->bomTemplates[$category] = $sourceItem->bom; $this->info(" ✅ {$category}: {$sourceCode} 템플릿 로드됨 (".count($bom).'개 항목)'); } else { $this->warn(" ⚠️ {$category}: {$sourceCode} BOM이 비어있음"); } } else { $this->warn(" ⚠️ {$category}: {$sourceCode} 템플릿 없음"); } } $this->newLine(); return count($this->bomTemplates) > 0; } /** * 대상 품목 조회 (BOM이 NULL인 FG 완제품) */ private function getTargetItems(int $tenantId, ?string $code, ?string $category) { $query = DB::table('items') ->where('tenant_id', $tenantId) ->where('item_type', 'FG') ->whereIn('item_category', array_keys($this->bomTemplates)) ->where(function ($q) { $q->whereNull('bom') ->orWhere('bom', '[]') ->orWhere('bom', 'null') ->orWhereRaw('JSON_LENGTH(bom) = 0'); }); if ($code) { $query->where('code', $code); } if ($category) { $query->where('item_category', strtoupper($category)); } return $query->get(['id', 'code', 'name', 'item_category']); } /** * 개별 품목에 BOM 적용 */ private function applyBomToItem(object $item, bool $dryRun): string { $category = $item->item_category; // 템플릿 확인 if (! isset($this->bomTemplates[$category])) { if ($this->output->isVerbose()) { $this->warn(" ⏭️ [{$item->code}] {$item->name} - {$category} 템플릿 없음"); } return 'skipped'; } $bomJson = $this->bomTemplates[$category]; if ($dryRun) { if ($this->output->isVerbose()) { $this->info(" 📋 [{$item->code}] {$item->name} → {$category} 템플릿 적용 예정"); } return 'success'; } // 실제 업데이트 try { DB::table('items') ->where('id', $item->id) ->update([ 'bom' => $bomJson, 'updated_at' => now(), ]); if ($this->output->isVerbose()) { $this->info(" ✅ [{$item->code}] {$item->name} → BOM 적용 완료"); } return 'success'; } catch (\Exception $e) { $this->error(" ❌ [{$item->code}] 오류: ".$e->getMessage()); return 'failed'; } } }