Files
sam-api/app/Console/Commands/BendingImportImages.php

170 lines
5.9 KiB
PHP
Raw Normal View History

<?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',
};
}
}