From 84ff9d7fd898d606e1cc37d75a2ef91fa54116ca Mon Sep 17 00:00:00 2001 From: hskwon Date: Thu, 11 Dec 2025 23:17:52 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Items=20API=20BOM=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=ED=99=95=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /items/{id} 응답에 BOM 확장 데이터 포함 - child_item_code, child_item_name, unit, specification 필드 추가 - expandBomData() 메서드 구현 (ItemsService) - Product 모델 bom 캐스팅 추가 --- app/Models/Products/Product.php | 3 +- app/Services/ItemsService.php | 155 ++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 1 deletion(-) diff --git a/app/Models/Products/Product.php b/app/Models/Products/Product.php index 6f18b7c..718fe46 100644 --- a/app/Models/Products/Product.php +++ b/app/Models/Products/Product.php @@ -17,7 +17,7 @@ class Product extends Model protected $fillable = [ 'tenant_id', 'code', 'name', 'unit', 'category_id', 'product_type', // 라벨/분류용 - 'attributes', 'attributes_archive', 'options', 'description', + 'attributes', 'attributes_archive', 'options', 'bom', 'description', 'is_sellable', 'is_purchasable', 'is_producible', // 하이브리드 구조: 최소 고정 필드 'safety_stock', 'lead_time', 'is_variable_size', @@ -34,6 +34,7 @@ class Product extends Model 'attributes' => 'array', 'attributes_archive' => 'array', 'options' => 'array', + 'bom' => 'array', 'bending_details' => 'array', 'certification_start_date' => 'date', 'certification_end_date' => 'date', diff --git a/app/Services/ItemsService.php b/app/Services/ItemsService.php index 25e4576..7c856c2 100644 --- a/app/Services/ItemsService.php +++ b/app/Services/ItemsService.php @@ -136,6 +136,147 @@ private function processDynamicOptions(array &$data, string $sourceTable): void } } + /** + * BOM 데이터에서 child_item_id, child_item_type, quantity만 추출 + * + * @param array|null $bomData BOM 데이터 배열 + * @return array|null [{child_item_id, child_item_type, quantity}, ...] + */ + private function extractBomData(?array $bomData): ?array + { + if (empty($bomData)) { + return null; + } + + $extracted = []; + foreach ($bomData as $item) { + if (! is_array($item)) { + continue; + } + + $childItemId = $item['child_item_id'] ?? null; + $childItemType = $item['child_item_type'] ?? $item['ref_type'] ?? 'PRODUCT'; + $quantity = $item['quantity'] ?? null; + + if ($childItemId === null) { + continue; + } + + // child_item_type 정규화 (PRODUCT/MATERIAL) + $childItemType = strtoupper($childItemType); + if (! in_array($childItemType, ['PRODUCT', 'MATERIAL'])) { + $childItemType = 'PRODUCT'; + } + + $extracted[] = [ + 'child_item_id' => (int) $childItemId, + 'child_item_type' => $childItemType, + 'quantity' => $quantity !== null ? (float) $quantity : 1, + ]; + } + + return empty($extracted) ? null : $extracted; + } + + /** + * BOM 데이터 확장 (child_item 상세 정보 포함) + * + * DB에 저장된 [{child_item_id, child_item_type, quantity}] 형태를 + * [{child_item_id, child_item_type, child_item_code, child_item_name, quantity, unit, specification?}] + * 형태로 확장하여 반환 + * + * @param array $bomData BOM 데이터 배열 [{child_item_id, child_item_type, quantity}, ...] + * @param int $tenantId 테넌트 ID + * @return array 확장된 BOM 데이터 + */ + private function expandBomData(array $bomData, int $tenantId): array + { + if (empty($bomData)) { + return []; + } + + // child_item_type별로 ID 분리 + $productIds = []; + $materialIds = []; + + foreach ($bomData as $item) { + $childId = $item['child_item_id'] ?? null; + $childType = strtoupper($item['child_item_type'] ?? 'PRODUCT'); + + if ($childId === null) { + continue; + } + + if ($childType === 'MATERIAL') { + $materialIds[] = $childId; + } else { + $productIds[] = $childId; + } + } + + // Products에서 조회 (FG, PT) + $products = collect([]); + if (! empty($productIds)) { + $products = Product::query() + ->where('tenant_id', $tenantId) + ->whereIn('id', $productIds) + ->get(['id', 'code', 'name', 'unit']) + ->keyBy('id'); + } + + // Materials에서 조회 (SM, RM, CS) + $materials = collect([]); + if (! empty($materialIds)) { + $materials = Material::query() + ->where('tenant_id', $tenantId) + ->whereIn('id', $materialIds) + ->get(['id', 'material_code', 'name', 'unit', 'specification']) + ->keyBy('id'); + } + + // BOM 데이터 확장 + $expanded = []; + foreach ($bomData as $item) { + $childId = $item['child_item_id'] ?? null; + $childType = strtoupper($item['child_item_type'] ?? 'PRODUCT'); + + if ($childId === null) { + continue; + } + + $result = [ + 'child_item_id' => (int) $childId, + 'child_item_type' => $childType, + 'quantity' => $item['quantity'] ?? 1, + ]; + + // child_item_type에 따라 조회 + if ($childType === 'MATERIAL' && isset($materials[$childId])) { + $material = $materials[$childId]; + $result['child_item_code'] = $material->material_code; + $result['child_item_name'] = $material->name; + $result['unit'] = $material->unit; + if ($material->specification) { + $result['specification'] = $material->specification; + } + } elseif ($childType === 'PRODUCT' && isset($products[$childId])) { + $product = $products[$childId]; + $result['child_item_code'] = $product->code; + $result['child_item_name'] = $product->name; + $result['unit'] = $product->unit; + } else { + // 해당하는 품목이 없으면 null 처리 + $result['child_item_code'] = null; + $result['child_item_name'] = null; + $result['unit'] = null; + } + + $expanded[] = $result; + } + + return $expanded; + } + /** * 통합 품목 조회 (materials + products UNION) * @@ -360,6 +501,11 @@ public function getItem( $data['item_type'] = $itemType; $data['type_code'] = $product->product_type; + // BOM 데이터 확장 (child_item 상세 정보 포함) + if (! empty($data['bom'])) { + $data['bom'] = $this->expandBomData($data['bom'], $tenantId); + } + // 가격 정보 추가 if ($includePrice) { $data['prices'] = $this->fetchPrices('PRODUCT', $id, $clientId, $priceDate); @@ -463,6 +609,9 @@ private function createProduct(array $data, int $tenantId, int $userId): Product // 동적 필드를 options에 병합 $this->processDynamicOptions($data, 'products'); + // BOM 데이터 처리 (child_item_id, quantity만 추출) + $bomData = $this->extractBomData($data['bom'] ?? null); + $payload = $data; $payload['tenant_id'] = $tenantId; $payload['created_by'] = $userId; @@ -470,6 +619,7 @@ private function createProduct(array $data, int $tenantId, int $userId): Product $payload['is_sellable'] = $payload['is_sellable'] ?? true; $payload['is_purchasable'] = $payload['is_purchasable'] ?? false; $payload['is_producible'] = $payload['is_producible'] ?? false; + $payload['bom'] = $bomData; return Product::create($payload); } @@ -564,6 +714,11 @@ private function updateProduct(int $id, array $data): Product // 동적 필드를 options에 병합 $this->processDynamicOptions($data, 'products'); + // BOM 데이터 처리 (bom 키가 있을 때만) + if (array_key_exists('bom', $data)) { + $data['bom'] = $this->extractBomData($data['bom']); + } + // item_type은 DB 필드가 아니므로 제거 unset($data['item_type']); $data['updated_by'] = $userId;