feat: 품목 치수 정규화 Artisan 커맨드 추가
- items:normalize-dimensions 커맨드 신규 생성 - 101_specification_1/2/3에서 thickness/width/length 자동 추출 - --dry-run(미리보기) / --execute(실행) 모드 지원 - 기존 값이 있는 경우 안전하게 스킵 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
218
app/Console/Commands/NormalizeItemDimensions.php
Normal file
218
app/Console/Commands/NormalizeItemDimensions.php
Normal file
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Attributes\AsCommand;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
#[AsCommand(name: 'items:normalize-dimensions', description: '품목 attributes에서 thickness/width/length 정규화')]
|
||||
class NormalizeItemDimensions extends Command
|
||||
{
|
||||
protected $signature = 'items:normalize-dimensions
|
||||
{--tenant_id=287 : Target tenant ID (default: 287 경동기업)}
|
||||
{--dry-run : 기본 모드 - 변경 예정 목록만 출력}
|
||||
{--execute : 실제 DB 업데이트 수행}';
|
||||
|
||||
protected $description = '101_specification_1/2/3에서 thickness/width/length를 추출하여 attributes에 정규화';
|
||||
|
||||
private int $updatedCount = 0;
|
||||
|
||||
private int $skippedCount = 0;
|
||||
|
||||
private array $changes = [];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$tenantId = (int) $this->option('tenant_id');
|
||||
$execute = $this->option('execute');
|
||||
$dryRun = ! $execute;
|
||||
|
||||
$this->info('=== 품목 치수 정규화 ===');
|
||||
$this->info("Tenant ID: {$tenantId}");
|
||||
$this->info('Mode: '.($dryRun ? 'DRY-RUN (미리보기)' : 'EXECUTE (실행)'));
|
||||
$this->newLine();
|
||||
|
||||
$items = DB::table('items')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereNull('deleted_at')
|
||||
->whereRaw("JSON_EXTRACT(attributes, '$.\"101_specification_1\"') IS NOT NULL")
|
||||
->get();
|
||||
|
||||
$this->info("대상 품목: {$items->count()}건 (101_specification_1 존재)");
|
||||
$this->newLine();
|
||||
|
||||
$bar = $this->output->createProgressBar($items->count());
|
||||
$bar->start();
|
||||
|
||||
foreach ($items as $item) {
|
||||
$this->processItem($item, $dryRun);
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
|
||||
$this->showResults($dryRun);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function processItem(object $item, bool $dryRun): void
|
||||
{
|
||||
$attributes = json_decode($item->attributes, true) ?? [];
|
||||
|
||||
$spec1 = $attributes['101_specification_1'] ?? null;
|
||||
$spec2 = $attributes['102_specification_2'] ?? null;
|
||||
$spec3 = $attributes['103_specification_3'] ?? null;
|
||||
|
||||
$existingThickness = $attributes['thickness'] ?? null;
|
||||
$existingWidth = $attributes['width'] ?? null;
|
||||
$existingLength = $attributes['length'] ?? null;
|
||||
|
||||
$changed = false;
|
||||
$changeDetails = [];
|
||||
|
||||
// thickness 추출: spec1에서 숫자 추출 (t/T 제거)
|
||||
if ($spec1 !== null && $spec1 !== '' && $existingThickness === null) {
|
||||
$thickness = $this->extractThickness($spec1);
|
||||
if ($thickness !== null) {
|
||||
$attributes['thickness'] = $thickness;
|
||||
$changeDetails[] = "thickness: {$spec1} → {$thickness}";
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// width 추출: spec2에서 순수 숫자만
|
||||
if ($spec2 !== null && $spec2 !== '' && $existingWidth === null) {
|
||||
$width = $this->extractNumeric($spec2);
|
||||
if ($width !== null) {
|
||||
$attributes['width'] = $width;
|
||||
$changeDetails[] = "width: {$spec2} → {$width}";
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// length 추출: spec3에서 순수 숫자만 (c, P/L, 문자 포함 시 스킵)
|
||||
if ($spec3 !== null && $spec3 !== '' && $existingLength === null) {
|
||||
$length = $this->extractLength($spec3);
|
||||
if ($length !== null) {
|
||||
$attributes['length'] = $length;
|
||||
$changeDetails[] = "length: {$spec3} → {$length}";
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($changed) {
|
||||
$this->changes[] = [
|
||||
'id' => $item->id,
|
||||
'name' => $item->name,
|
||||
'changes' => implode(', ', $changeDetails),
|
||||
];
|
||||
|
||||
if (! $dryRun) {
|
||||
DB::table('items')
|
||||
->where('id', $item->id)
|
||||
->update(['attributes' => json_encode($attributes, JSON_UNESCAPED_UNICODE)]);
|
||||
}
|
||||
|
||||
$this->updatedCount++;
|
||||
} else {
|
||||
$this->skippedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* thickness 추출: "t1.2", "T1.2", "1.2", "egi1.17" → 숫자
|
||||
* 패턴: 선행 문자(t/T/영문) 제거 후 숫자 추출
|
||||
*/
|
||||
private function extractThickness(?string $value): ?string
|
||||
{
|
||||
if ($value === null || trim($value) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cleaned = trim($value);
|
||||
|
||||
// "t1.2", "T1.2" → "1.2"
|
||||
$cleaned = preg_replace('/^[tT]/', '', $cleaned);
|
||||
|
||||
// "egi1.17", "sus1.2" → 영문자 제거 후 숫자 추출
|
||||
if (preg_match('/(\d+(?:\.\d+)?)/', $cleaned, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 순수 숫자만 추출 (정수/소수)
|
||||
* "1219" → "1219", "1219.5" → "1219.5"
|
||||
* "c" → null, "" → null, "P/L" → null
|
||||
*/
|
||||
private function extractNumeric(?string $value): ?string
|
||||
{
|
||||
if ($value === null || trim($value) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cleaned = trim($value);
|
||||
|
||||
// 순수 숫자 (정수/소수)만 허용
|
||||
if (preg_match('/^\d+(?:\.\d+)?$/', $cleaned)) {
|
||||
return $cleaned;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* length 추출: "3000" → "3000", "3000 P/L" → "3000", "c" → null
|
||||
* 선행 숫자가 있고 뒤에 공백+문자(P/L 등)가 붙는 경우 숫자만 추출
|
||||
*/
|
||||
private function extractLength(?string $value): ?string
|
||||
{
|
||||
if ($value === null || trim($value) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cleaned = trim($value);
|
||||
|
||||
// 순수 숫자
|
||||
if (preg_match('/^\d+(?:\.\d+)?$/', $cleaned)) {
|
||||
return $cleaned;
|
||||
}
|
||||
|
||||
// "3000 P/L" → "3000" (숫자로 시작하고 뒤에 공백+문자)
|
||||
if (preg_match('/^(\d+(?:\.\d+)?)\s+/', $cleaned, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
// "c", "P/L" 등 숫자 없는 경우
|
||||
return null;
|
||||
}
|
||||
|
||||
private function showResults(bool $dryRun): void
|
||||
{
|
||||
$this->info('=== 결과 ===');
|
||||
$this->info("변경 대상: {$this->updatedCount}건");
|
||||
$this->info("스킵 (변경 불필요): {$this->skippedCount}건");
|
||||
$this->newLine();
|
||||
|
||||
if (! empty($this->changes)) {
|
||||
$this->table(
|
||||
['ID', '품목명', '변경 내용'],
|
||||
array_map(fn ($c) => [$c['id'], mb_substr($c['name'], 0, 30), $c['changes']], $this->changes)
|
||||
);
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->newLine();
|
||||
$this->warn('DRY-RUN 모드입니다. 실제 적용하려면 --execute 옵션을 사용하세요:');
|
||||
$this->line(' php artisan items:normalize-dimensions --execute');
|
||||
} else {
|
||||
$this->newLine();
|
||||
$this->info("총 {$this->updatedCount}건 업데이트 완료");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user