['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'SUS마감재', 'material' => 'SUS 1.2T'], 'RE' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'EGI마감재', 'material' => 'EGI 1.55T'], 'RM' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => '본체', 'material' => 'EGI 1.55T'], 'RC' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'C형', 'material' => 'EGI 1.55T'], 'RD' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'D형', 'material' => 'EGI 1.55T'], 'RT' => ['item_sep' => '철재', 'item_bending' => '가이드레일', 'item_name' => '본체(철재)', 'material' => 'EGI 1.55T'], // 가이드레일 (측면) 'SS' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'SUS마감재', 'material' => 'SUS 1.2T'], 'SE' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'EGI마감재', 'material' => 'EGI 1.55T'], 'SM' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => '본체', 'material' => 'EGI 1.55T'], 'SC' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'C형', 'material' => 'EGI 1.55T'], 'SD' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'D형', 'material' => 'EGI 1.55T'], 'ST' => ['item_sep' => '철재', 'item_bending' => '가이드레일', 'item_name' => '본체(철재)', 'material' => 'EGI 1.55T'], 'SU' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'SUS마감재2', 'material' => 'SUS 1.2T'], // 하단마감재 'BE' => ['item_sep' => '스크린', 'item_bending' => '하단마감재', 'item_name' => '하단마감재', 'material' => 'EGI 1.55T'], 'BS' => ['item_sep' => '스크린', 'item_bending' => '하단마감재', 'item_name' => '하단마감재', 'material' => 'SUS 1.5T'], 'TS' => ['item_sep' => '철재', 'item_bending' => '하단마감재', 'item_name' => '하단마감재(철재)', 'material' => 'SUS 1.2T'], 'LA' => ['item_sep' => '스크린', 'item_bending' => 'L-BAR', 'item_name' => 'L-Bar', 'material' => 'EGI 1.55T'], 'HH' => ['item_sep' => '스크린', 'item_bending' => '보강평철', 'item_name' => '보강평철', 'material' => 'EGI 1.55T'], // 셔터박스 'CF' => ['item_sep' => '스크린', 'item_bending' => '케이스', 'item_name' => '전면부', 'material' => 'EGI 1.55T'], 'CL' => ['item_sep' => '스크린', 'item_bending' => '케이스', 'item_name' => '린텔부', 'material' => 'EGI 1.55T'], 'CP' => ['item_sep' => '스크린', 'item_bending' => '케이스', 'item_name' => '점검구', 'material' => 'EGI 1.55T'], 'CB' => ['item_sep' => '스크린', 'item_bending' => '케이스', 'item_name' => '후면코너부', 'material' => 'EGI 1.55T'], // 연기차단재 'GI' => ['item_sep' => '스크린', 'item_bending' => '연기차단재', 'item_name' => '연기차단재', 'material' => '화이바원단'], // 공용 'XX' => ['item_sep' => '스크린', 'item_bending' => '공용', 'item_name' => '하부BASE/상부덮개/마구리', 'material' => 'EGI 1.55T'], 'YY' => ['item_sep' => '스크린', 'item_bending' => '별도마감', 'item_name' => '별도SUS마감', 'material' => 'SUS 1.2T'], ]; // 한글 패턴 → 분류 매핑 private const KOREAN_PATTERN_META = [ 'BD-가이드레일' => ['item_sep' => null, 'item_bending' => '가이드레일'], 'BD-케이스' => ['item_sep' => null, 'item_bending' => '케이스'], 'BD-마구리' => ['item_sep' => null, 'item_bending' => '마구리'], 'BD-하단마감재' => ['item_sep' => null, 'item_bending' => '하단마감재'], 'BD-L-BAR' => ['item_sep' => '스크린', 'item_bending' => 'L-BAR'], 'BD-보강평철' => ['item_sep' => '스크린', 'item_bending' => '보강평철'], ]; private const LENGTH_MAP = [ '02' => 200, '12' => 1219, '24' => 2438, '30' => 3000, '35' => 3500, '40' => 4000, '41' => 4150, '42' => 4200, '43' => 4300, '53' => 3000, '54' => 4000, '83' => 3000, '84' => 4000, ]; private array $stats = [ 'total' => 0, 'prefix_len_filled' => 0, 'korean_filled' => 0, 'already_complete' => 0, 'unknown_pattern' => 0, ]; public function handle(): int { $tenantId = (int) $this->option('tenant_id'); $dryRun = $this->option('dry-run'); $this->info('=== BD-* 품목 options 보강 ==='); $this->info('Tenant: '.$tenantId.' | Mode: '.($dryRun ? 'DRY-RUN' : 'LIVE')); $this->newLine(); // BD-* 전체 품목 조회 $items = DB::table('items') ->where('tenant_id', $tenantId) ->where('code', 'like', 'BD-%') ->whereNull('deleted_at') ->select('id', 'code', 'name', 'options') ->orderBy('code') ->get(); $this->stats['total'] = $items->count(); $this->info("BD-* 품목: {$items->count()}건"); $this->newLine(); foreach ($items as $item) { $options = json_decode($item->options ?? '{}', true) ?: []; $code = $item->code; $newOptions = $this->resolveOptions($code, $item->name, $options); if ($newOptions === null) { $this->stats['unknown_pattern']++; $this->warn(" ❓ 미인식 패턴: {$code}"); continue; } // 변경 필요 여부 확인 $merged = array_merge($options, $newOptions); if ($merged == $options) { $this->stats['already_complete']++; continue; } if (! $dryRun) { $encoded = json_encode($merged, JSON_UNESCAPED_UNICODE); if ($encoded === false) { $this->error(" ❌ JSON 인코딩 실패: {$code} — ".json_last_error_msg()); continue; } DB::table('items') ->where('id', $item->id) ->update([ 'options' => $encoded, 'updated_at' => now(), ]); } $pattern = $this->detectPattern($code); if ($pattern === 'prefix_len') { $this->stats['prefix_len_filled']++; } else { $this->stats['korean_filled']++; } $this->line(" ✅ {$code}: +".implode(', ', array_keys($newOptions))); } $this->showStats($dryRun); return self::SUCCESS; } /** * 코드에서 options 속성 추출 */ private function resolveOptions(string $code, string $name, array $existing): ?array { $new = []; // item_category 보장 if (empty($existing['item_category'])) { // item_category는 items 테이블 컬럼이므로 여기서는 skip } // 패턴 A: BD-PREFIX-LEN (예: BD-RS-30) if (preg_match('/^BD-([A-Z]{2})-(\d{2})$/', $code, $m)) { $prefix = $m[1]; $lengthCode = $m[2]; // prefix/length 기본값 if (empty($existing['prefix'])) { $new['prefix'] = $prefix; } if (empty($existing['length_code'])) { $new['length_code'] = $lengthCode; } if (empty($existing['length_mm']) && isset(self::LENGTH_MAP[$lengthCode])) { $new['length_mm'] = self::LENGTH_MAP[$lengthCode]; } // PREFIX 기반 분류 속성 $meta = self::PREFIX_META[$prefix] ?? null; if ($meta) { foreach ($meta as $key => $value) { if (empty($existing[$key])) { $new[$key] = $value; } } } return $new; } // 특수 코드 (패턴 미준수) $specialCodes = [ 'BD-가이드레일용 연기차단재' => ['item_bending' => '연기차단재'], 'BD-케이스용 연기차단재' => ['item_bending' => '연기차단재'], ]; if (isset($specialCodes[$code])) { foreach ($specialCodes[$code] as $key => $value) { if (empty($existing[$key])) { $new[$key] = $value; } } return $new; } // 패턴 B~G: 한글 패턴 foreach (self::KOREAN_PATTERN_META as $patternPrefix => $meta) { // 정확한 접두사+구분자 매칭 (BD-케이스-xxx는 O, BD-케이스용xxx는 X) if ($code === $patternPrefix || str_starts_with($code, $patternPrefix.'-')) { // 분류 속성 foreach ($meta as $key => $value) { if ($value !== null && empty($existing[$key])) { $new[$key] = $value; } } // 한글 패턴별 추가 파싱 $this->parseKoreanPattern($code, $patternPrefix, $existing, $new); return $new; } } return null; } /** * 한글 패턴에서 모델/재질/규격 추출 */ private function parseKoreanPattern(string $code, string $patternPrefix, array $existing, array &$new): void { $suffix = substr($code, strlen($patternPrefix) + 1); // "-" 제거 $parts = explode('-', $suffix); switch ($patternPrefix) { case 'BD-가이드레일': // BD-가이드레일-KSS01-SUS-120*70 if (count($parts) >= 3) { if (empty($existing['model_name'])) { $new['model_name'] = $parts[0]; } if (empty($existing['material'])) { $material = $parts[1]; $new['material'] = str_contains($material, 'SUS') ? 'SUS 1.2T' : 'EGI 1.55T'; } if (empty($existing['item_spec'])) { $new['item_spec'] = $parts[2]; } // item_sep 추론 (KTE → 철재) if (empty($existing['item_sep'])) { $new['item_sep'] = str_starts_with($parts[0], 'KTE') ? '철재' : '스크린'; } } break; case 'BD-하단마감재': // BD-하단마감재-KSS01-SUS-60*40 if (count($parts) >= 3) { if (empty($existing['model_name'])) { $new['model_name'] = $parts[0]; } if (empty($existing['material'])) { $material = $parts[1]; $new['material'] = str_contains($material, 'SUS') ? 'SUS 1.5T' : 'EGI 1.55T'; } if (empty($existing['item_spec'])) { $new['item_spec'] = $parts[2]; } if (empty($existing['item_sep'])) { $new['item_sep'] = str_starts_with($parts[0], 'KTE') ? '철재' : '스크린'; } } break; case 'BD-케이스': // BD-케이스-650*550 if (count($parts) >= 1 && ! empty($parts[0])) { if (empty($existing['item_spec'])) { $new['item_spec'] = $parts[0]; } // 케이스는 대부분 철재 if (empty($existing['item_sep'])) { $new['item_sep'] = '철재'; } } break; case 'BD-마구리': // BD-마구리-655*505 if (count($parts) >= 1 && ! empty($parts[0])) { if (empty($existing['item_spec'])) { $new['item_spec'] = $parts[0]; } if (empty($existing['item_sep'])) { $new['item_sep'] = '철재'; } } break; case 'BD-L-BAR': // BD-L-BAR-KSS01-17*60 if (count($parts) >= 2) { if (empty($existing['model_name'])) { $new['model_name'] = $parts[0]; } if (empty($existing['item_spec'])) { $new['item_spec'] = $parts[1]; } } break; case 'BD-보강평철': // BD-보강평철-50 if (count($parts) >= 1 && ! empty($parts[0])) { if (empty($existing['item_spec'])) { $new['item_spec'] = $parts[0]; } } break; } } private function detectPattern(string $code): string { return preg_match('/^BD-[A-Z]{2}-\d{2}$/', $code) ? 'prefix_len' : 'korean'; } private function showStats(bool $dryRun): void { $this->newLine(); $this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); $this->info('📊 결과'.($dryRun ? ' (DRY-RUN)' : '')); $this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); $this->info(" 전체 BD-* 품목: {$this->stats['total']}건"); $this->info(" PREFIX-LEN 업데이트: {$this->stats['prefix_len_filled']}건"); $this->info(" 한글 패턴 업데이트: {$this->stats['korean_filled']}건"); $this->info(" 이미 완료: {$this->stats['already_complete']}건"); if ($this->stats['unknown_pattern'] > 0) { $this->warn(" 미인식 패턴: {$this->stats['unknown_pattern']}건"); } $this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); if ($dryRun) { $this->newLine(); $this->info('🔍 DRY-RUN 완료. --dry-run 제거하면 실제 반영됩니다.'); } } }