[ 'name' => '슬랫', 'code_patterns' => [], 'name_keywords' => ['철재용', '철재', '슬랫'], 'name_excludes' => ['스크린', '가이드레일', '하단마감', '연기차단', '케이스'], // 재고생산 품목 제외 ], 'P-002' => [ 'name' => '스크린', 'code_patterns' => [], 'name_keywords' => ['스크린용', '스크린', '원단', '실리카', '방충', '와이어'], 'name_excludes' => ['가이드레일', '하단마감', '연기차단', '케이스'], // 재고생산 품목 제외 ], 'P-003' => [ 'name' => '절곡', 'code_patterns' => ['BD-%'], // BD 코드는 절곡 'name_keywords' => ['절곡'], // 절곡 키워드만 (나머지는 P-004로) 'name_excludes' => [], ], 'P-004' => [ 'name' => '재고생산', 'code_patterns' => ['PT-%'], // PT 코드는 재고생산 부품 'name_keywords' => ['가이드레일', '케이스', '연기차단', 'L-Bar', 'L-BAR', 'LBar', '하단마감', '린텔', '하장바'], 'name_excludes' => [], 'code_excludes' => ['BD-%'], // BD 코드는 P-003으로 ], ]; public function handle(): int { $tenantId = $this->option('tenant'); $dryRun = $this->option('dry-run'); $clear = $this->option('clear'); $this->info('=== 5130 기준 품목-공정 매핑 (A+B+C 전략) ==='); $this->info('A. 품목명 키워드: 스크린용→P-002, 철재용→P-001'); $this->info('B. BD 코드: BD-* → P-003 절곡'); $this->info('C. 재고생산: PT-* 또는 가이드레일/케이스/연기차단재/L-Bar → P-004'); $this->newLine(); // 공정 조회 $processQuery = Process::query(); if ($tenantId) { $processQuery->where('tenant_id', $tenantId); } $processes = $processQuery->whereIn('process_code', array_keys($this->mappingRules))->get()->keyBy('process_code'); if ($processes->isEmpty()) { $this->error('매핑 대상 공정이 없습니다. (P-001, P-002, P-003, P-004)'); return self::FAILURE; } $this->info('대상 공정:'); foreach ($processes as $code => $process) { $this->line(" - {$code}: {$process->process_name} (ID: {$process->id})"); } $this->newLine(); // 기존 매핑 삭제 (--clear 옵션) if ($clear) { $processIds = $processes->pluck('id')->toArray(); $existingCount = ProcessItem::whereIn('process_id', $processIds)->count(); if ($dryRun) { $this->warn("[DRY-RUN] 기존 매핑 {$existingCount}개 삭제 예정"); } else { ProcessItem::whereIn('process_id', $processIds)->delete(); $this->warn("기존 매핑 {$existingCount}개 삭제 완료"); } $this->newLine(); } // 매핑 결과 저장 $results = [ 'P-001' => ['items' => collect(), 'process' => $processes->get('P-001')], 'P-002' => ['items' => collect(), 'process' => $processes->get('P-002')], 'P-003' => ['items' => collect(), 'process' => $processes->get('P-003')], 'P-004' => ['items' => collect(), 'process' => $processes->get('P-004')], ]; // 품목 조회 및 분류 $itemQuery = Item::query(); if ($tenantId) { $itemQuery->where('tenant_id', $tenantId); } $items = $itemQuery->get(); $this->info("전체 품목 수: {$items->count()}개"); $this->newLine(); $mappedCount = 0; $unmappedItems = collect(); foreach ($items as $item) { $processCode = $this->classifyItem($item); if ($processCode && isset($results[$processCode])) { $results[$processCode]['items']->push($item); $mappedCount++; } else { $unmappedItems->push($item); } } // 결과 출력 $this->info('=== 분류 결과 ==='); $this->newLine(); $tableData = []; foreach ($results as $code => $data) { $count = $data['items']->count(); $processName = $data['process']?->process_name ?? '(없음)'; $tableData[] = [$code, $processName, $count]; } $tableData[] = ['-', '미분류', $unmappedItems->count()]; $tableData[] = ['=', '합계', $items->count()]; $this->table(['공정코드', '공정명', '품목 수'], $tableData); $this->newLine(); // 샘플 출력 foreach ($results as $code => $data) { if ($data['items']->isNotEmpty()) { $this->info("[{$code} {$data['process']?->process_name}] 샘플 (최대 10개):"); foreach ($data['items']->take(10) as $item) { $this->line(" - {$item->code}: {$item->name}"); } $this->newLine(); } } // 미분류 샘플 if ($unmappedItems->isNotEmpty()) { $this->info("[미분류] 샘플 (최대 10개):"); foreach ($unmappedItems->take(10) as $item) { $this->line(" - {$item->code}: {$item->name}"); } $this->newLine(); } // 실제 매핑 실행 if (! $dryRun) { $this->info('=== 매핑 실행 ==='); DB::transaction(function () use ($results) { foreach ($results as $code => $data) { $process = $data['process']; if (! $process) { continue; } $priority = 0; foreach ($data['items'] as $item) { // 중복 체크 $exists = ProcessItem::where('process_id', $process->id) ->where('item_id', $item->id) ->exists(); if (! $exists) { ProcessItem::create([ 'process_id' => $process->id, 'item_id' => $item->id, 'priority' => $priority++, 'is_active' => true, ]); } } $this->info(" {$code}: {$data['items']->count()}개 매핑 완료"); } }); $this->newLine(); $this->info("총 {$mappedCount}개 품목 매핑 완료!"); } else { $this->newLine(); $this->warn('[DRY-RUN] 실제 매핑은 수행되지 않았습니다.'); $this->line('실제 실행: php artisan items:map-to-processes --clear'); } return self::SUCCESS; } /** * 품목을 공정에 분류 (A+B+C 전략) */ private function classifyItem(Item $item): ?string { $code = $item->code ?? ''; $name = $item->name ?? ''; // B. BD 코드 → P-003 절곡 (최우선) if (str_starts_with($code, 'BD-')) { return 'P-003'; } // C. PT 코드 → P-004 재고생산 (코드 기반 우선) if (str_starts_with($code, 'PT-')) { return 'P-004'; } // C. P-004 재고생산 키워드 체크 (가이드레일, 케이스, 연기차단재, L-Bar, 하단마감, 린텔) foreach ($this->mappingRules['P-004']['name_keywords'] as $keyword) { if (mb_stripos($name, $keyword) !== false) { return 'P-004'; } } // A. 품목명 키워드 기반 분류 // P-002 스크린 먼저 체크 (스크린용, 스크린 키워드) foreach ($this->mappingRules['P-002']['name_keywords'] as $keyword) { if (mb_stripos($name, $keyword) !== false) { // 재고생산 품목 제외 $excluded = false; foreach ($this->mappingRules['P-002']['name_excludes'] as $exclude) { if (mb_stripos($name, $exclude) !== false) { $excluded = true; break; } } if (! $excluded) { return 'P-002'; } } } // P-001 슬랫 체크 (철재용, 철재, 슬랫 키워드) foreach ($this->mappingRules['P-001']['name_keywords'] as $keyword) { if (mb_stripos($name, $keyword) !== false) { // 재고생산 품목 제외 $excluded = false; foreach ($this->mappingRules['P-001']['name_excludes'] as $exclude) { if (mb_stripos($name, $exclude) !== false) { $excluded = true; break; } } if (! $excluded) { return 'P-001'; } } } // P-003 절곡 키워드 체크 (BD 코드 외에 키워드로도 분류) foreach ($this->mappingRules['P-003']['name_keywords'] as $keyword) { if (mb_stripos($name, $keyword) !== false) { return 'P-003'; } } return null; } }