fix: 경동 BOM 계산 수정 및 품목-공정 매핑

- KyungdongFormulaHandler: product_type 자동 추론(item_category 기반), 철재 주자재 EGI코일로 변경, 조인트바 steel 공통 지원
- FormulaEvaluatorService: FG item_category에서 product_type 자동 판별
- MapItemsToProcesses: 경동 품목-공정 매핑 커맨드 정비
- KyungdongItemMasterSeeder: BOM child_item_id code 기반 재매핑
- ItemsBomController: ghost ID 유효성 검증 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 20:58:47 +09:00
parent 55270198d4
commit 10b1b26c1b
8 changed files with 18184 additions and 18048 deletions

View File

@@ -41,31 +41,37 @@ class MapItemsToProcesses extends Command
* - G: 연기차단재
* - L: L-Bar
*/
/**
* FG(완제품), RM(원자재) 제외 - 공정별 생산 품목만 매핑
* EST-INSPECTION(검사비), EST-MOTOR/EST-CTRL(구매품)도 제외
*/
private array $globalExcludes = ['FG-%', 'RM-%', 'EST-INSPECTION'];
private array $mappingRules = [
'P-001' => [
'name' => '슬랫',
'code_patterns' => [],
'name_keywords' => ['철재용', '철재', '슬랫'],
'name_excludes' => ['스크린', '가이드레일', '하단마감', '연기차단', '케이스'], // 재고생산 품목 제외
'code_patterns' => ['EST-RAW-슬랫-%'], // 슬랫 원자재 (방화/방범/조인트바)
'name_keywords' => ['슬랫'],
'name_excludes' => ['스크린', '가이드레일', '하단마감', '연기차단', '케이스'],
],
'P-002' => [
'name' => '스크린',
'code_patterns' => [],
'code_patterns' => ['EST-RAW-스크린-%'], // 스크린 원자재 (실리카/와이어 등)
'name_keywords' => ['스크린용', '스크린', '원단', '실리카', '방충', '와이어'],
'name_excludes' => ['가이드레일', '하단마감', '연기차단', '케이스'], // 재고생산 품목 제외
'name_excludes' => ['가이드레일', '하단마감', '연기차단', '케이스'],
],
'P-003' => [
'name' => '절곡',
'code_patterns' => ['BD-%'], // BD 코드는 절곡
'name_keywords' => ['절곡'], // 절곡 키워드만 (나머지는 P-004로)
'name_keywords' => ['절곡', '연기차단재'], // 연기차단재는 절곡 공정에서 조립
'name_excludes' => [],
],
'P-004' => [
'name' => '재고생산',
'code_patterns' => ['PT-%'], // PT 코드는 재고생산 부품
'name_keywords' => ['가이드레일', '케이스', '연기차단', 'L-Bar', 'L-BAR', 'LBar', '하단마감', '린텔', '하장바'],
'name_keywords' => ['가이드레일', '케이스', 'L-Bar', 'L-BAR', 'LBar', '하단마감', '린텔', '하장바', '환봉', '감기샤프트', '각파이프', '앵글'],
'name_excludes' => [],
'code_excludes' => ['BD-%'], // BD 코드는 P-003으로
'code_excludes' => ['BD-%', 'EST-SMOKE-%'], // BD는 P-003, EST-SMOKE는 P-003
],
];
@@ -175,7 +181,7 @@ public function handle(): int
// 미분류 샘플
if ($unmappedItems->isNotEmpty()) {
$this->info("[미분류] 샘플 (최대 10개):");
$this->info('[미분류] 샘플 (최대 10개):');
foreach ($unmappedItems->take(10) as $item) {
$this->line(" - {$item->code}: {$item->name}");
}
@@ -233,28 +239,78 @@ private function classifyItem(Item $item): ?string
$code = $item->code ?? '';
$name = $item->name ?? '';
// B. BD 코드 → P-003 절곡 (최우선)
// 0. 글로벌 제외 (FG 완제품, RM 원자재, EST-INSPECTION 서비스)
foreach ($this->globalExcludes as $excludePattern) {
$prefix = rtrim($excludePattern, '-%');
if (str_starts_with($code, $prefix)) {
return null;
}
}
// 1. 코드 패턴 우선 매핑 (정확한 매칭)
// EST-RAW-슬랫-* → P-001
if (str_starts_with($code, 'EST-RAW-슬랫-')) {
return 'P-001';
}
// EST-RAW-스크린-* → P-002
if (str_starts_with($code, 'EST-RAW-스크린-')) {
return 'P-002';
}
// BD-* → P-003 절곡
if (str_starts_with($code, 'BD-')) {
return 'P-003';
}
// C. PT 코드 → P-004 재고생산 (코드 기반 우선)
// EST-SMOKE-* → P-003 절곡 (연기차단재는 절곡 공정에서 조립)
if (str_starts_with($code, 'EST-SMOKE-')) {
return 'P-003';
}
// PT-* → P-004 재고생산
if (str_starts_with($code, 'PT-')) {
return 'P-004';
}
// C. P-004 재고생산 키워드 체크 (가이드레일, 케이스, 연기차단재, L-Bar, 하단마감, 린텔)
// EST-MOTOR/EST-CTRL → 구매품, 공정 없음
if (str_starts_with($code, 'EST-MOTOR-') || str_starts_with($code, 'EST-CTRL-')) {
return null;
}
// EST-SHAFT/EST-PIPE/EST-ANGLE → P-004 재고생산 (조달 품목)
if (str_starts_with($code, 'EST-SHAFT-') || str_starts_with($code, 'EST-PIPE-') || str_starts_with($code, 'EST-ANGLE-')) {
return 'P-004';
}
// 2. P-004 재고생산 키워드 체크
foreach ($this->mappingRules['P-004']['name_keywords'] as $keyword) {
if (mb_stripos($name, $keyword) !== false) {
return 'P-004';
// code_excludes 체크
$excluded = false;
foreach ($this->mappingRules['P-004']['code_excludes'] ?? [] as $excludePattern) {
$prefix = rtrim($excludePattern, '-%');
if (str_starts_with($code, $prefix)) {
$excluded = true;
break;
}
}
if (! $excluded) {
return 'P-004';
}
}
}
// A. 품목명 키워드 기반 분류
// P-002 스크린 먼저 체크 (스크린용, 스크린 키워드)
// 3. P-003 절곡 키워드 체크
foreach ($this->mappingRules['P-003']['name_keywords'] as $keyword) {
if (mb_stripos($name, $keyword) !== false) {
return 'P-003';
}
}
// 4. 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) {
@@ -268,10 +324,9 @@ private function classifyItem(Item $item): ?string
}
}
// P-001 슬랫 체크 (철재용, 철재, 슬랫 키워드)
// 5. 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) {
@@ -285,13 +340,6 @@ private function classifyItem(Item $item): ?string
}
}
// P-003 절곡 키워드 체크 (BD 코드 외에 키워드로도 분류)
foreach ($this->mappingRules['P-003']['name_keywords'] as $keyword) {
if (mb_stripos($name, $keyword) !== false) {
return 'P-003';
}
}
return null;
}
}