option('tenant_id'); $dryRun = $this->option('dry-run'); $sourceBase = rtrim($this->option('source'), '/'); $this->info('=== 레거시 절곡 이미지 → R2 마이그레이션 ==='); $this->info('Source: '.$sourceBase); $this->info('Mode: '.($dryRun ? 'DRY-RUN' : 'LIVE')); $this->newLine(); // 1. BENDING 아이템에서 legacy_bending_num이 있는 것 조회 $items = DB::table('items') ->where('tenant_id', $tenantId) ->where('item_category', 'BENDING') ->whereNull('deleted_at') ->get(['id', 'code', 'options']); $this->info("BENDING 아이템: {$items->count()}건"); // legacy_bending_num → chandj imgdata 매핑 $chandjImages = DB::connection('chandj')->table('bending') ->whereNull('is_deleted') ->whereNotNull('imgdata') ->where('imgdata', '!=', '') ->pluck('imgdata', 'num'); $this->info("chandj 이미지: {$chandjImages->count()}건"); $this->newLine(); foreach ($items as $item) { $opts = json_decode($item->options ?? '{}', true) ?: []; $legacyNum = $opts['legacy_bending_num'] ?? null; if (! $legacyNum || ! isset($chandjImages[$legacyNum])) { continue; } // 이미 파일이 연결되어 있으면 스킵 $existingFile = File::where('tenant_id', $tenantId) ->where('document_type', '1') ->where('document_id', $item->id) ->where('field_key', 'bending_diagram') ->whereNull('deleted_at') ->first(); if ($existingFile) { $this->skipped++; continue; } $imgFilename = $chandjImages[$legacyNum]; $imageUrl = "{$sourceBase}/{$imgFilename}"; if ($dryRun) { $this->line(" ✅ {$item->code} ← {$imgFilename}"); $this->uploaded++; continue; } // 이미지 다운로드 try { $response = Http::withoutVerifying()->timeout(15)->get($imageUrl); if (! $response->successful()) { $this->warn(" ❌ {$item->code}: HTTP {$response->status()} ({$imageUrl})"); $this->failed++; continue; } $imageContent = $response->body(); $mimeType = $response->header('Content-Type', 'image/png'); $extension = $this->getExtension($imgFilename, $mimeType); // R2 저장 $storedName = bin2hex(random_bytes(8)).'.'.$extension; $year = date('Y'); $month = date('m'); $directory = sprintf('%d/items/%s/%s', $tenantId, $year, $month); $filePath = $directory.'/'.$storedName; Storage::disk('r2')->put($filePath, $imageContent); // files 테이블 저장 $file = File::create([ 'tenant_id' => $tenantId, 'display_name' => $imgFilename, 'stored_name' => $storedName, 'file_path' => $filePath, 'file_size' => strlen($imageContent), 'mime_type' => $mimeType, 'file_type' => 'image', 'field_key' => 'bending_diagram', 'document_id' => $item->id, 'document_type' => '1', 'is_temp' => false, 'uploaded_by' => 1, 'created_by' => 1, ]); $this->line(" ✅ {$item->code} ← {$imgFilename} → file_id={$file->id}"); $this->uploaded++; } catch (\Exception $e) { $this->error(" ❌ {$item->code}: {$e->getMessage()}"); $this->failed++; } } $this->newLine(); $this->info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); $this->info("업로드: {$this->uploaded}건 | 스킵(이미 있음): {$this->skipped}건 | 실패: {$this->failed}건"); if ($dryRun) { $this->info('🔍 DRY-RUN 완료.'); } return self::SUCCESS; } private function getExtension(string $filename, string $mimeType): string { $ext = pathinfo($filename, PATHINFO_EXTENSION); if ($ext) { return strtolower($ext); } return match ($mimeType) { 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', default => 'png', }; } }