diff --git a/app/Console/Commands/NormalizeItemDimensions.php b/app/Console/Commands/NormalizeItemDimensions.php new file mode 100644 index 0000000..1d268a6 --- /dev/null +++ b/app/Console/Commands/NormalizeItemDimensions.php @@ -0,0 +1,218 @@ +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}건 업데이트 완료"); + } + } +}