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

@@ -76,7 +76,7 @@ public function run(): void
private function cleanup(): void
{
$this->command->info('CLEANUP: tenant_id=' . $this->tenantId . ' 데이터 삭제');
$this->command->info('CLEANUP: tenant_id='.$this->tenantId.' 데이터 삭제');
// FK 역순 삭제
// prices - tenant_id로 직접 삭제
@@ -100,7 +100,7 @@ private function cleanup(): void
->whereNotNull('parent_id')
->delete();
$c = DB::table('categories')->where('tenant_id', $this->tenantId)->delete();
$this->command->info(" categories: " . count($catIds) . "건 삭제");
$this->command->info(' categories: '.count($catIds).'건 삭제');
// entity_relationships
$c = DB::table('entity_relationships')->where('tenant_id', $this->tenantId)->delete();
@@ -140,7 +140,7 @@ private function seedItemPages(): void
$this->pageMap[$originalId] = $newId;
}
$this->command->info(" item_pages: " . count($rows) . "건 삽입");
$this->command->info(' item_pages: '.count($rows).'건 삽입');
}
private function seedItemSections(): void
@@ -158,7 +158,7 @@ private function seedItemSections(): void
$this->sectionMap[$originalId] = $newId;
}
$this->command->info(" item_sections: " . count($rows) . "건 삽입");
$this->command->info(' item_sections: '.count($rows).'건 삽입');
}
private function seedItemFields(): void
@@ -176,7 +176,7 @@ private function seedItemFields(): void
$this->fieldMap[$originalId] = $newId;
}
$this->command->info(" item_fields: " . count($rows) . "건 삽입");
$this->command->info(' item_fields: '.count($rows).'건 삽입');
}
private function seedEntityRelationships(): void
@@ -197,6 +197,7 @@ private function seedEntityRelationships(): void
if ($data['parent_id'] === null || $data['child_id'] === null) {
$this->command->warn(" entity_relationships: 매핑 실패 건 스킵 (parent_type={$row['parent_type']}, child_type={$row['child_type']})");
continue;
}
@@ -314,6 +315,7 @@ private function seedCategories(): void
if (! isset($this->categoryMap[$parentOriginalId])) {
$nextRemaining[] = $row;
continue;
}
@@ -329,10 +331,10 @@ private function seedCategories(): void
}
if (! empty($remaining)) {
$this->command->warn(" categories: " . count($remaining) . "건 매핑 실패 (depth 초과)");
$this->command->warn(' categories: '.count($remaining).'건 매핑 실패 (depth 초과)');
}
$this->command->info(" categories: " . count($this->categoryMap) . "건 삽입");
$this->command->info(' categories: '.count($this->categoryMap).'건 삽입');
}
private function seedItems(): void
@@ -369,7 +371,57 @@ private function seedItems(): void
->pluck('id', 'code')
->toArray();
$this->command->info(" items: {$total}건 삽입 (itemMap: " . count($this->itemMap) . "건)");
$this->command->info(" items: {$total}건 삽입 (itemMap: ".count($this->itemMap).'건)');
// BOM의 child_item_code → child_item_id 변환
$this->remapBomCodes();
}
/**
* BOM JSON의 child_item_code를 실제 child_item_id로 변환
*
* items.json의 bom 필드가 child_item_code(품목코드) 기반으로 저장되어 있으므로,
* INSERT 후 itemMap(code→id)을 이용해 child_item_id로 교체한다.
*/
private function remapBomCodes(): void
{
$bomItems = DB::table('items')
->where('tenant_id', $this->tenantId)
->whereNotNull('bom')
->get(['id', 'code', 'bom']);
$updated = 0;
foreach ($bomItems as $item) {
$bom = json_decode($item->bom, true);
if (! is_array($bom)) {
continue;
}
$changed = false;
foreach ($bom as &$entry) {
// child_item_code → child_item_id 변환
if (isset($entry['child_item_code'])) {
$code = $entry['child_item_code'];
if (isset($this->itemMap[$code])) {
$entry['child_item_id'] = $this->itemMap[$code];
unset($entry['child_item_code']);
$changed = true;
} else {
$this->command->warn(" BOM 매핑 실패: {$item->code}{$code} (품목 미존재)");
}
}
}
unset($entry);
if ($changed) {
DB::table('items')->where('id', $item->id)
->update(['bom' => json_encode($bom, JSON_UNESCAPED_UNICODE)]);
$updated++;
}
}
$this->command->info(" BOM code→id 매핑: {$updated}건 변환");
}
// ──────────────────────────────────────────────────────────────────
@@ -399,6 +451,7 @@ private function seedItemDetails(): void
$code = $originalIdToCode[$originalItemId] ?? null;
if ($code === null || ! isset($this->itemMap[$code])) {
$skipped++;
continue;
}
@@ -417,7 +470,7 @@ private function seedItemDetails(): void
}
$this->expectedCounts['item_details'] = count($insertData);
$this->command->info(" item_details: " . count($insertData) . "건 삽입");
$this->command->info(' item_details: '.count($insertData).'건 삽입');
}
private function seedPrices(): void
@@ -445,6 +498,7 @@ private function seedPrices(): void
$code = $originalIdToCode[$originalItemId] ?? null;
if ($code === null || ! isset($this->itemMap[$code])) {
$skipped++;
continue;
}
@@ -463,7 +517,7 @@ private function seedPrices(): void
}
$this->expectedCounts['prices'] = count($insertData);
$this->command->info(" prices: " . count($insertData) . "건 삽입");
$this->command->info(' prices: '.count($insertData).'건 삽입');
}
// ──────────────────────────────────────────────────────────────────
@@ -528,7 +582,7 @@ private function verifyRelationshipIntegrity(): void
if ($orphans > 0) {
$this->command->warn(" entity_relationships 참조 무결성: {$orphans}건 고아 레코드");
} else {
$this->command->info(" entity_relationships 참조 무결성: OK");
$this->command->info(' entity_relationships 참조 무결성: OK');
}
}
@@ -547,12 +601,12 @@ private function printSummary(): void
{
$this->command->newLine();
$this->command->info('=== 경동기업 품목 기준 데이터 시더 완료 ===');
$this->command->info(" 매핑 현황:");
$this->command->info(" pageMap: " . count($this->pageMap) . "");
$this->command->info(" sectionMap: " . count($this->sectionMap) . "");
$this->command->info(" fieldMap: " . count($this->fieldMap) . "");
$this->command->info(" categoryMap: " . count($this->categoryMap) . "");
$this->command->info(" itemMap: " . count($this->itemMap) . "");
$this->command->info(' 매핑 현황:');
$this->command->info(' pageMap: '.count($this->pageMap).'건');
$this->command->info(' sectionMap: '.count($this->sectionMap).'건');
$this->command->info(' fieldMap: '.count($this->fieldMap).'건');
$this->command->info(' categoryMap: '.count($this->categoryMap).'건');
$this->command->info(' itemMap: '.count($this->itemMap).'건');
}
// ──────────────────────────────────────────────────────────────────
@@ -561,11 +615,11 @@ private function printSummary(): void
private function loadJson(string $filename): array
{
$path = $this->dataPath . '/' . $filename;
$path = $this->dataPath.'/'.$filename;
if (! file_exists($path)) {
$this->command->error("JSON 파일 없음: {$path}");
$this->command->error("먼저 php artisan kyungdong:export-item-master 를 실행하세요.");
$this->command->error('먼저 php artisan kyungdong:export-item-master 를 실행하세요.');
throw new \RuntimeException("JSON 파일 없음: {$path}");
}

File diff suppressed because it is too large Load Diff