From d2bdecf063760b9cc939bdb92b0e547746a6ebdf Mon Sep 17 00:00:00 2001 From: kent Date: Sat, 13 Dec 2025 23:53:16 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20BOM=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BOM child_item_id를 새 items 테이블 ID로 마이그레이션 - Item.loadBomChildren() 수정: setRelation()으로 모델에 설정 - ItemService.validateBom() 추가: 순환 참조 방지 - error.php에 self_reference_bom 메시지 추가 - ID 7 자기참조 BOM 데이터 수정 완료 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Models/Items/Item.php | 13 +- app/Services/ItemService.php | 27 ++++ ...ate_bom_child_item_ids_to_new_item_ids.php | 130 ++++++++++++++++++ lang/ko/error.php | 1 + 4 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 database/migrations/2025_12_13_160000_update_bom_child_item_ids_to_new_item_ids.php diff --git a/app/Models/Items/Item.php b/app/Models/Items/Item.php index d342526..8a25c58 100644 --- a/app/Models/Items/Item.php +++ b/app/Models/Items/Item.php @@ -184,15 +184,20 @@ public function getBomChildIds(): array } /** - * BOM 자식 품목들 조회 (Eager Loading 최적화) + * BOM 자식 품목들 조회 및 모델에 설정 */ - public function loadBomChildren() + public function loadBomChildren(): self { $childIds = $this->getBomChildIds(); if (empty($childIds)) { - return collect(); + $this->setRelation('bom_children', collect()); + + return $this; } - return self::whereIn('id', $childIds)->get()->keyBy('id'); + $children = self::whereIn('id', $childIds)->get()->keyBy('id'); + $this->setRelation('bom_children', $children); + + return $this; } } \ No newline at end of file diff --git a/app/Services/ItemService.php b/app/Services/ItemService.php index 471218d..fc4cb6d 100644 --- a/app/Services/ItemService.php +++ b/app/Services/ItemService.php @@ -190,6 +190,28 @@ private function normalizeOptions(?array $in): ?array return $out ?: null; } + /** + * BOM 검증 (순환 참조 방지) + * + * @param array|null $bom BOM 데이터 + * @param int|null $itemId 현재 품목 ID (수정 시) + */ + private function validateBom(?array $bom, ?int $itemId = null): void + { + if (empty($bom)) { + return; + } + + foreach ($bom as $entry) { + $childItemId = $entry['child_item_id'] ?? null; + + // 자기 자신 참조 방지 + if ($itemId && $childItemId == $itemId) { + throw new BadRequestHttpException(__('error.item.self_reference_bom')); + } + } + } + /** * 카테고리 트리 전체 조회 */ @@ -430,6 +452,11 @@ public function update(int $id, array $data): Model } } + // BOM 검증 (순환 참조 방지) + if (isset($data['bom'])) { + $this->validateBom($data['bom'], $id); + } + // 테이블 업데이트 $itemData = array_intersect_key($data, array_flip([ 'item_type', 'code', 'name', 'unit', 'category_id', diff --git a/database/migrations/2025_12_13_160000_update_bom_child_item_ids_to_new_item_ids.php b/database/migrations/2025_12_13_160000_update_bom_child_item_ids_to_new_item_ids.php new file mode 100644 index 0000000..e5c4ce1 --- /dev/null +++ b/database/migrations/2025_12_13_160000_update_bom_child_item_ids_to_new_item_ids.php @@ -0,0 +1,130 @@ +whereNotNull('bom') + ->whereRaw('JSON_LENGTH(bom) > 0') + ->get(['id', 'bom']); + + $updated = 0; + $skipped = 0; + + foreach ($items as $item) { + $bom = json_decode($item->bom, true); + if (empty($bom)) { + continue; + } + + $newBom = []; + $hasChanges = false; + + foreach ($bom as $entry) { + $childItemId = $entry['child_item_id'] ?? null; + $childItemType = $entry['child_item_type'] ?? null; + + if (! $childItemId || ! $childItemType) { + $newBom[] = $entry; + continue; + } + + // child_item_type에 따른 source_table 결정 + $sourceTable = match (strtoupper($childItemType)) { + 'MATERIAL' => 'materials', + 'PRODUCT', 'SUBASSEMBLY', 'PART' => 'products', + default => null, + }; + + if (! $sourceTable) { + $newBom[] = $entry; + continue; + } + + // item_id_mappings에서 새 item_id 조회 + $mapping = DB::table('item_id_mappings') + ->where('source_table', $sourceTable) + ->where('source_id', $childItemId) + ->first(); + + if ($mapping) { + $entry['child_item_id'] = $mapping->item_id; + $hasChanges = true; + } + + $newBom[] = $entry; + } + + if ($hasChanges) { + DB::table('items') + ->where('id', $item->id) + ->update(['bom' => json_encode($newBom)]); + $updated++; + } else { + $skipped++; + } + } + + // 결과 로그 + DB::statement("SELECT 'BOM migration: updated={$updated}, skipped={$skipped}' AS result"); + } + + /** + * 롤백: 새 item_id를 이전 ID로 복원 + * 주의: 완벽한 롤백은 어려움 - 원본 BOM 백업 권장 + */ + public function down(): void + { + // BOM이 있는 모든 items 조회 + $items = DB::table('items') + ->whereNotNull('bom') + ->whereRaw('JSON_LENGTH(bom) > 0') + ->get(['id', 'bom']); + + foreach ($items as $item) { + $bom = json_decode($item->bom, true); + if (empty($bom)) { + continue; + } + + $newBom = []; + $hasChanges = false; + + foreach ($bom as $entry) { + $childItemId = $entry['child_item_id'] ?? null; + + if (! $childItemId) { + $newBom[] = $entry; + continue; + } + + // item_id_mappings에서 원본 ID 조회 + $mapping = DB::table('item_id_mappings') + ->where('item_id', $childItemId) + ->first(); + + if ($mapping) { + $entry['child_item_id'] = $mapping->source_id; + $hasChanges = true; + } + + $newBom[] = $entry; + } + + if ($hasChanges) { + DB::table('items') + ->where('id', $item->id) + ->update(['bom' => json_encode($newBom)]); + } + } + } +}; diff --git a/lang/ko/error.php b/lang/ko/error.php index e72e729..42815d9 100644 --- a/lang/ko/error.php +++ b/lang/ko/error.php @@ -124,6 +124,7 @@ 'batch_in_use' => '사용 중인 품목이 포함되어 있어 삭제할 수 없습니다. (품목: :codes, :count건)', 'invalid_item_type' => '유효하지 않은 품목 유형입니다.', 'duplicate_code' => '중복된 품목 코드입니다.', + 'self_reference_bom' => 'BOM에 자기 자신을 포함할 수 없습니다.', ], // 잠금 관련