170 lines
5.9 KiB
PHP
170 lines
5.9 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace App\Console\Commands;
|
||
|
|
|
||
|
|
use App\Models\Commons\File;
|
||
|
|
use Illuminate\Console\Attributes\AsCommand;
|
||
|
|
use Illuminate\Console\Command;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
use Illuminate\Support\Facades\Http;
|
||
|
|
use Illuminate\Support\Facades\Storage;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 레거시 5130 절곡 이미지 → SAM R2 + files 테이블 마이그레이션
|
||
|
|
*
|
||
|
|
* 소스: https://5130.codebridge-x.com/bending/img/{imgdata}
|
||
|
|
* 대상: R2 저장 + files 테이블 + items.options 업데이트
|
||
|
|
*/
|
||
|
|
#[AsCommand(name: 'bending:import-images', description: '레거시 절곡 이미지 → R2 마이그레이션')]
|
||
|
|
class BendingImportImages extends Command
|
||
|
|
{
|
||
|
|
protected $signature = 'bending:import-images
|
||
|
|
{--tenant_id=287 : Target tenant ID}
|
||
|
|
{--dry-run : 실제 저장 없이 미리보기}
|
||
|
|
{--source=https://5130.codebridge-x.com/bending/img : 이미지 소스 URL}';
|
||
|
|
|
||
|
|
private int $uploaded = 0;
|
||
|
|
|
||
|
|
private int $skipped = 0;
|
||
|
|
|
||
|
|
private int $failed = 0;
|
||
|
|
|
||
|
|
public function handle(): int
|
||
|
|
{
|
||
|
|
$tenantId = (int) $this->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',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|