option('tenant_id'); $dryRun = $this->option('dry-run'); // bending_items의 legacy_bending_id → 이미지 파일 매핑 $itemImageMap = []; $items = BendingItem::where('tenant_id', $tenantId) ->whereNotNull('legacy_bending_id') ->get(); foreach ($items as $bi) { $file = File::where('document_type', 'bending_item') ->where('document_id', $bi->id) ->where('field_key', 'bending_diagram') ->whereNull('deleted_at') ->first(); if ($file) { $itemImageMap[$bi->legacy_bending_id] = $file; } } $this->info("기초관리 이미지 매핑: " . count($itemImageMap) . "건"); $models = BendingModel::where('tenant_id', $tenantId) ->whereNotNull('components') ->get(); $copied = 0; $skipped = 0; $noSource = 0; foreach ($models as $model) { $components = $model->components; if (empty($components)) { continue; } $updated = false; foreach ($components as $idx => &$comp) { // 이미 image_file_id가 있으면 skip if (! empty($comp['image_file_id'])) { $skipped++; continue; } // source_num으로 기초관리 이미지 찾기 $sourceNum = $comp['num'] ?? $comp['source_num'] ?? null; if (! $sourceNum) { $noSource++; continue; } $sourceFile = $itemImageMap[(int) $sourceNum] ?? null; if (! $sourceFile || ! $sourceFile->file_path) { $noSource++; continue; } if ($dryRun) { $this->line(" [DRY] model#{$model->id} comp[{$idx}] ← bending#{$sourceNum} file#{$sourceFile->id}"); $copied++; continue; } // R2에서 파일 복사 try { $newFile = $this->copyFile($sourceFile, $model->id, $tenantId); $comp['image_file_id'] = $newFile->id; $updated = true; $copied++; } catch (\Throwable $e) { $this->warn(" ⚠️ 복사 실패: model#{$model->id} comp[{$idx}] — {$e->getMessage()}"); } } unset($comp); if ($updated && ! $dryRun) { $model->components = $components; $model->save(); } } $this->newLine(); $this->info("완료: 복사 {$copied}건, 스킵 {$skipped}건, 소스없음 {$noSource}건"); return 0; } private function copyFile(File $source, int $modelId, int $tenantId): File { $extension = pathinfo($source->stored_name, PATHINFO_EXTENSION); $storedName = bin2hex(random_bytes(8)) . '.' . $extension; $directory = sprintf('%d/bending/model-parts/%s/%s', $tenantId, date('Y'), date('m')); $newPath = $directory . '/' . $storedName; // R2 파일 복사 $content = Storage::disk('r2')->get($source->file_path); Storage::disk('r2')->put($newPath, $content); // 새 파일 레코드 생성 return File::create([ 'tenant_id' => $tenantId, 'display_name' => $source->display_name, 'stored_name' => $storedName, 'file_path' => $newPath, 'file_size' => $source->file_size, 'mime_type' => $source->mime_type, 'file_type' => 'image', 'field_key' => 'component_image', 'document_id' => $modelId, 'document_type' => 'bending_model', 'is_temp' => false, 'uploaded_by' => 1, 'created_by' => 1, ]); } }