option('tenant_id'); $dryRun = $this->option('dry-run'); $sourceBase = rtrim($this->option('source'), '/'); $this->info('=== 결합형태 이미지 → R2 마이그레이션 ==='); // 3개 JSON 파일 순차 처리 $jsonConfigs = [ ['file' => 'guiderail/guiderail.json', 'category' => 'GUIDERAIL_MODEL', 'imageBase' => ''], ['file' => 'shutterbox/shutterbox.json', 'category' => 'SHUTTERBOX_MODEL', 'imageBase' => ''], ['file' => 'bottombar/bottombar.json', 'category' => 'BOTTOMBAR_MODEL', 'imageBase' => ''], ]; $uploaded = 0; $skipped = 0; $failed = 0; foreach ($jsonConfigs as $jsonConfig) { $jsonPath = base_path('../5130/' . $jsonConfig['file']); if (! file_exists($jsonPath)) { $resp = Http::withoutVerifying()->get("{$sourceBase}/{$jsonConfig['file']}"); $assemblyData = $resp->successful() ? $resp->json() : []; } else { $assemblyData = json_decode(file_get_contents($jsonPath), true) ?: []; } $this->info("--- {$jsonConfig['category']} ({$jsonConfig['file']}): " . count($assemblyData) . '건 ---'); foreach ($assemblyData as $entry) { $imagePath = $entry['image'] ?? ''; if (! $imagePath) { continue; } // SAM 코드 생성 (카테고리별) $code = $this->buildCode($entry, $jsonConfig['category']); if (! $code) { continue; } $samItem = DB::table('items') ->where('tenant_id', $tenantId) ->where('code', $code) ->where('item_category', $jsonConfig['category']) ->whereNull('deleted_at') ->first(['id', 'code', 'options']); if (! $samItem) { $this->warn(" ⚠️ {$code}: SAM 모델 없음"); $failed++; continue; } // 이미 이미지 있으면 스킵 $existing = File::where('tenant_id', $tenantId) ->where('document_id', $samItem->id) ->where('field_key', 'assembly_image') ->whereNull('deleted_at') ->first(); if ($existing) { $skipped++; continue; } $imageUrl = "{$sourceBase}{$imagePath}"; if ($dryRun) { $this->line(" ✅ {$code} ← {$imagePath}"); $uploaded++; continue; } try { $response = Http::withoutVerifying()->timeout(15)->get($imageUrl); if (! $response->successful()) { $this->warn(" ❌ {$code}: HTTP {$response->status()}"); $failed++; continue; } $content = $response->body(); $ext = pathinfo($imagePath, PATHINFO_EXTENSION) ?: 'png'; $storedName = bin2hex(random_bytes(8)).'.'.strtolower($ext); $directory = sprintf('%d/items/%s/%s', $tenantId, date('Y'), date('m')); $filePath = $directory.'/'.$storedName; Storage::disk('r2')->put($filePath, $content); $file = File::create([ 'tenant_id' => $tenantId, 'display_name' => basename($imagePath), 'stored_name' => $storedName, 'file_path' => $filePath, 'file_size' => strlen($content), 'mime_type' => $response->header('Content-Type', 'image/png'), 'file_type' => 'image', 'field_key' => 'assembly_image', 'document_id' => $samItem->id, 'document_type' => '1', 'is_temp' => false, 'uploaded_by' => 1, 'created_by' => 1, ]); $this->line(" ✅ {$code} ← {$imagePath} → file_id={$file->id}"); $uploaded++; } catch (\Exception $e) { $this->error(" ❌ {$code}: {$e->getMessage()}"); $failed++; } } } // end foreach jsonConfigs $this->newLine(); $this->info("업로드: {$uploaded}건 | 스킵: {$skipped}건 | 실패: {$failed}건"); return self::SUCCESS; } private function buildCode(array $entry, string $category): ?string { if ($category === 'GUIDERAIL_MODEL') { $modelName = $entry['model_name'] ?? ''; $checkType = $entry['check_type'] ?? ''; $finishType = $entry['finishing_type'] ?? ''; if (! $modelName) { return null; } $finish = str_contains($finishType, 'SUS') ? 'SUS' : 'EGI'; return "GR-{$modelName}-{$checkType}-{$finish}"; } if ($category === 'SHUTTERBOX_MODEL') { $w = $entry['box_width'] ?? ''; $h = $entry['box_height'] ?? ''; $exit = $entry['exit_direction'] ?? ''; $exitShort = match ($exit) { '양면 점검구' => '양면', '밑면 점검구' => '밑면', '후면 점검구' => '후면', default => $exit, }; return "SB-{$w}*{$h}-{$exitShort}"; } if ($category === 'BOTTOMBAR_MODEL') { $modelName = $entry['model_name'] ?? ''; $finishType = $entry['finishing_type'] ?? ''; if (! $modelName) { return null; } $finish = str_contains($finishType, 'SUS') ? 'SUS' : 'EGI'; return "BB-{$modelName}-{$finish}"; } return null; } }