Files
sam-api/app/Console/Commands/BendingModelImportAssemblyImages.php
강영보 7083057d59 feat: [bending] 절곡품 관리 API 완성 + 데이터 마이그레이션
- GuiderailModelController/Service/Resource: 가이드레일/케이스/하단마감재 통합 CRUD
- item_category 필터 (GUIDERAIL_MODEL/SHUTTERBOX_MODEL/BOTTOMBAR_MODEL)
- BendingItemResource: legacy_bending_num 노출 추가
- ApiKeyMiddleware: guiderail-models, files 화이트리스트 추가
- Swagger: BendingItemApi, GuiderailModelApi 문서 (케이스/하단마감재 필드 포함)
- 마이그레이션 커맨드 5개: GuiderailImportLegacy, BendingProductImportLegacy, BendingImportImages, BendingModelImportImages, BendingModelImportAssemblyImages
- 데이터: GR 20건 + SB 30건 + BB 10건 + 이미지 473건 R2 업로드
2026-03-17 12:50:26 +09:00

193 lines
6.7 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;
/**
* 레거시 guiderail.json 결합형태 이미지 → SAM 모델 연결
*/
#[AsCommand(name: 'bending-model:import-assembly-images', description: '결합형태 이미지 → R2 마이그레이션')]
class BendingModelImportAssemblyImages extends Command
{
protected $signature = 'bending-model:import-assembly-images
{--tenant_id=287 : Target tenant ID}
{--dry-run : 실제 저장 없이 미리보기}
{--source=https://5130.codebridge-x.com : 소스 URL}';
public function handle(): int
{
$tenantId = (int) $this->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;
}
}