option('tenant_id'); $dryRun = $this->option('dry-run'); $sourceBase = rtrim($this->option('source'), '/'); $this->info('=== 절곡품 모델 부품 이미지 → R2 마이그레이션 ==='); $this->info('Mode: '.($dryRun ? 'DRY-RUN' : 'LIVE')); $this->newLine(); // chandj에서 원본 imgdata 조회 $chandjTables = [ 'GUIDERAIL_MODEL' => 'guiderail', 'SHUTTERBOX_MODEL' => 'shutterbox', 'BOTTOMBAR_MODEL' => 'bottombar', ]; foreach ($chandjTables as $category => $table) { $this->info("--- {$category} ({$table}) ---"); $chandjRows = DB::connection('chandj')->table($table)->whereNull('is_deleted')->get(); $samItems = DB::table('items')->where('tenant_id', $tenantId) ->where('item_category', $category)->whereNull('deleted_at') ->get(['id', 'code', 'options']); // legacy_num → chandj row 매핑 $chandjMap = []; foreach ($chandjRows as $row) { $chandjMap[$row->num] = $row; } foreach ($samItems as $samItem) { $opts = json_decode($samItem->options, true) ?? []; $legacyNum = $opts['legacy_num'] ?? $opts['legacy_guiderail_num'] ?? null; if (! $legacyNum || ! isset($chandjMap[$legacyNum])) { continue; } $chandjRow = $chandjMap[$legacyNum]; $chandjComps = json_decode($chandjRow->bending_components ?? '[]', true) ?: []; $components = $opts['components'] ?? []; $updated = false; foreach ($components as $idx => &$comp) { // chandj component에서 imgdata 찾기 $chandjComp = $chandjComps[$idx] ?? null; $imgdata = $chandjComp['imgdata'] ?? null; if (! $imgdata || ! empty($comp['image_file_id'])) { continue; } $imageUrl = "{$sourceBase}/{$imgdata}"; if ($dryRun) { $this->line(" ✅ {$samItem->code} #{$idx} ← {$imgdata}"); $this->uploaded++; $updated = true; continue; } try { $response = Http::withoutVerifying()->timeout(15)->get($imageUrl); if (! $response->successful()) { $this->warn(" ❌ {$samItem->code} #{$idx}: HTTP {$response->status()}"); $this->failed++; continue; } $imageContent = $response->body(); $extension = pathinfo($imgdata, PATHINFO_EXTENSION) ?: 'png'; $storedName = bin2hex(random_bytes(8)).'.'.strtolower($extension); $directory = sprintf('%d/items/%s/%s', $tenantId, date('Y'), date('m')); $filePath = $directory.'/'.$storedName; Storage::disk('r2')->put($filePath, $imageContent); $file = File::create([ 'tenant_id' => $tenantId, 'display_name' => $imgdata, 'stored_name' => $storedName, 'file_path' => $filePath, 'file_size' => strlen($imageContent), 'mime_type' => $response->header('Content-Type', 'image/png'), 'file_type' => 'image', 'field_key' => 'bending_component_image', 'document_id' => $samItem->id, 'document_type' => '1', 'is_temp' => false, 'uploaded_by' => 1, 'created_by' => 1, ]); $comp['image_file_id'] = $file->id; $comp['imgdata'] = $imgdata; $updated = true; $this->uploaded++; $this->line(" ✅ {$samItem->code} #{$idx} {$comp['itemName']} ← {$imgdata} → file_id={$file->id}"); } catch (\Exception $e) { $this->error(" ❌ {$samItem->code} #{$idx}: {$e->getMessage()}"); $this->failed++; } } unset($comp); // components 업데이트 if ($updated && ! $dryRun) { $opts['components'] = $components; DB::table('items')->where('id', $samItem->id)->update([ 'options' => json_encode($opts, JSON_UNESCAPED_UNICODE), 'updated_at' => now(), ]); } } } $this->newLine(); $this->info("업로드: {$this->uploaded}건 | 스킵: {$this->skipped}건 | 실패: {$this->failed}건"); if ($dryRun) { $this->info('🔍 DRY-RUN 완료.'); } return self::SUCCESS; } }