feat: Items API BOM 데이터 확장 기능 추가
- GET /items/{id} 응답에 BOM 확장 데이터 포함
- child_item_code, child_item_name, unit, specification 필드 추가
- expandBomData() 메서드 구현 (ItemsService)
- Product 모델 bom 캐스팅 추가
This commit is contained in:
@@ -17,7 +17,7 @@ class Product extends Model
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'tenant_id', 'code', 'name', 'unit', 'category_id',
|
'tenant_id', 'code', 'name', 'unit', 'category_id',
|
||||||
'product_type', // 라벨/분류용
|
'product_type', // 라벨/분류용
|
||||||
'attributes', 'attributes_archive', 'options', 'description',
|
'attributes', 'attributes_archive', 'options', 'bom', 'description',
|
||||||
'is_sellable', 'is_purchasable', 'is_producible',
|
'is_sellable', 'is_purchasable', 'is_producible',
|
||||||
// 하이브리드 구조: 최소 고정 필드
|
// 하이브리드 구조: 최소 고정 필드
|
||||||
'safety_stock', 'lead_time', 'is_variable_size',
|
'safety_stock', 'lead_time', 'is_variable_size',
|
||||||
@@ -34,6 +34,7 @@ class Product extends Model
|
|||||||
'attributes' => 'array',
|
'attributes' => 'array',
|
||||||
'attributes_archive' => 'array',
|
'attributes_archive' => 'array',
|
||||||
'options' => 'array',
|
'options' => 'array',
|
||||||
|
'bom' => 'array',
|
||||||
'bending_details' => 'array',
|
'bending_details' => 'array',
|
||||||
'certification_start_date' => 'date',
|
'certification_start_date' => 'date',
|
||||||
'certification_end_date' => 'date',
|
'certification_end_date' => 'date',
|
||||||
|
|||||||
@@ -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)
|
* 통합 품목 조회 (materials + products UNION)
|
||||||
*
|
*
|
||||||
@@ -360,6 +501,11 @@ public function getItem(
|
|||||||
$data['item_type'] = $itemType;
|
$data['item_type'] = $itemType;
|
||||||
$data['type_code'] = $product->product_type;
|
$data['type_code'] = $product->product_type;
|
||||||
|
|
||||||
|
// BOM 데이터 확장 (child_item 상세 정보 포함)
|
||||||
|
if (! empty($data['bom'])) {
|
||||||
|
$data['bom'] = $this->expandBomData($data['bom'], $tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
// 가격 정보 추가
|
// 가격 정보 추가
|
||||||
if ($includePrice) {
|
if ($includePrice) {
|
||||||
$data['prices'] = $this->fetchPrices('PRODUCT', $id, $clientId, $priceDate);
|
$data['prices'] = $this->fetchPrices('PRODUCT', $id, $clientId, $priceDate);
|
||||||
@@ -463,6 +609,9 @@ private function createProduct(array $data, int $tenantId, int $userId): Product
|
|||||||
// 동적 필드를 options에 병합
|
// 동적 필드를 options에 병합
|
||||||
$this->processDynamicOptions($data, 'products');
|
$this->processDynamicOptions($data, 'products');
|
||||||
|
|
||||||
|
// BOM 데이터 처리 (child_item_id, quantity만 추출)
|
||||||
|
$bomData = $this->extractBomData($data['bom'] ?? null);
|
||||||
|
|
||||||
$payload = $data;
|
$payload = $data;
|
||||||
$payload['tenant_id'] = $tenantId;
|
$payload['tenant_id'] = $tenantId;
|
||||||
$payload['created_by'] = $userId;
|
$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_sellable'] = $payload['is_sellable'] ?? true;
|
||||||
$payload['is_purchasable'] = $payload['is_purchasable'] ?? false;
|
$payload['is_purchasable'] = $payload['is_purchasable'] ?? false;
|
||||||
$payload['is_producible'] = $payload['is_producible'] ?? false;
|
$payload['is_producible'] = $payload['is_producible'] ?? false;
|
||||||
|
$payload['bom'] = $bomData;
|
||||||
|
|
||||||
return Product::create($payload);
|
return Product::create($payload);
|
||||||
}
|
}
|
||||||
@@ -564,6 +714,11 @@ private function updateProduct(int $id, array $data): Product
|
|||||||
// 동적 필드를 options에 병합
|
// 동적 필드를 options에 병합
|
||||||
$this->processDynamicOptions($data, 'products');
|
$this->processDynamicOptions($data, 'products');
|
||||||
|
|
||||||
|
// BOM 데이터 처리 (bom 키가 있을 때만)
|
||||||
|
if (array_key_exists('bom', $data)) {
|
||||||
|
$data['bom'] = $this->extractBomData($data['bom']);
|
||||||
|
}
|
||||||
|
|
||||||
// item_type은 DB 필드가 아니므로 제거
|
// item_type은 DB 필드가 아니므로 제거
|
||||||
unset($data['item_type']);
|
unset($data['item_type']);
|
||||||
$data['updated_by'] = $userId;
|
$data['updated_by'] = $userId;
|
||||||
|
|||||||
Reference in New Issue
Block a user