- 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 업로드
193 lines
6.7 KiB
PHP
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;
|
|
}
|
|
}
|