fix : BOM구성 API, DB 작업
- product_components 컬럼 변경 - BOM 구성, 카테고리리스트, BOM트리(재귀)호출 API 개발
This commit is contained in:
@@ -5,8 +5,11 @@
|
||||
use App\Models\Materials\Material;
|
||||
use App\Models\Products\Product;
|
||||
use App\Models\Products\ProductComponent;
|
||||
use App\Services\Products\ProductComponentResolver;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
@@ -332,4 +335,188 @@ private function assertReference(int $tenantId, int $parentProductId, string $re
|
||||
if (!$ok) throw new BadRequestHttpException(__('error.not_found'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 제품의 BOM을 전체 교체(기존 삭제 → 새 데이터 일괄 삽입)
|
||||
* - $productId: products.id
|
||||
* - $payload: ['categories' => [ {id?, name?, items: [{ref_type, ref_id, quantity, sort_order?}, ...]}, ... ]]
|
||||
* 반환: ['deleted_count' => int, 'inserted_count' => int]
|
||||
*/
|
||||
public function replaceBom(int $productId, array $payload): array
|
||||
{
|
||||
if ($productId <= 0) {
|
||||
throw new BadRequestHttpException(__('error.bad_request')); // 400
|
||||
}
|
||||
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
// 0) ====== 빈 카테고리 제거 ======
|
||||
$rawCats = Arr::get($payload, 'categories', []);
|
||||
$normalized = [];
|
||||
|
||||
foreach ((array) $rawCats as $cat) {
|
||||
$catId = Arr::get($cat, 'id');
|
||||
$catName = Arr::get($cat, 'name');
|
||||
|
||||
$items = array_values(array_filter((array) Arr::get($cat, 'items', []), function ($it) {
|
||||
$type = Arr::get($it, 'ref_type');
|
||||
$id = (int) Arr::get($it, 'ref_id');
|
||||
$qty = Arr::get($it, 'quantity');
|
||||
|
||||
return in_array($type, ['MATERIAL','PRODUCT'], true)
|
||||
&& $id > 0
|
||||
&& is_numeric($qty);
|
||||
}));
|
||||
|
||||
if (count($items) === 0) {
|
||||
continue; // 아이템 없으면 skip
|
||||
}
|
||||
|
||||
$normalized[] = [
|
||||
'id' => $catId,
|
||||
'name' => $catName,
|
||||
'items' => $items,
|
||||
];
|
||||
}
|
||||
|
||||
// 🔕 전부 비었으면: 기존 BOM 전체 삭제 후 성공
|
||||
if (count($normalized) === 0) {
|
||||
$deleted = ProductComponent::where('tenant_id', $tenantId)
|
||||
->where('parent_product_id', $productId)
|
||||
->delete();
|
||||
|
||||
return [
|
||||
'deleted_count' => $deleted,
|
||||
'inserted_count' => 0,
|
||||
'message' => '모든 BOM 항목이 비어 기존 데이터를 삭제했습니다.',
|
||||
];
|
||||
}
|
||||
|
||||
// 1) ====== 검증 ======
|
||||
$v = Validator::make(
|
||||
['categories' => $normalized],
|
||||
[
|
||||
'categories' => ['required', 'array', 'min:1'],
|
||||
'categories.*.id' => ['nullable', 'integer'],
|
||||
'categories.*.name' => ['nullable', 'string', 'max:100'],
|
||||
'categories.*.items' => ['required', 'array', 'min:1'],
|
||||
'categories.*.items.*.ref_type' => ['required', 'in:MATERIAL,PRODUCT'],
|
||||
'categories.*.items.*.ref_id' => ['required', 'integer', 'min:1'],
|
||||
'categories.*.items.*.quantity' => ['required', 'numeric', 'min:0'],
|
||||
'categories.*.items.*.sort_order' => ['nullable', 'integer', 'min:0'],
|
||||
]
|
||||
);
|
||||
if ($v->fails()) {
|
||||
throw new ValidationException($v, null, __('error.validation_failed'));
|
||||
}
|
||||
|
||||
// 2) ====== 플랫 레코드 생성 (note 제거) ======
|
||||
$rows = [];
|
||||
$now = now();
|
||||
foreach ($normalized as $cat) {
|
||||
$catId = Arr::get($cat, 'id');
|
||||
$catName = Arr::get($cat, 'name');
|
||||
|
||||
foreach ($cat['items'] as $idx => $item) {
|
||||
$rows[] = [
|
||||
'tenant_id' => $tenantId,
|
||||
'parent_product_id' => $productId,
|
||||
'category_id' => $catId,
|
||||
'category_name' => $catName,
|
||||
'ref_type' => $item['ref_type'],
|
||||
'ref_id' => (int) $item['ref_id'],
|
||||
'quantity' => (string) $item['quantity'],
|
||||
'sort_order' => isset($item['sort_order']) ? (int)$item['sort_order'] : $idx,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 3) ====== 트랜잭션: 기존 삭제 후 신규 삽입 ======
|
||||
return DB::transaction(function () use ($tenantId, $productId, $rows) {
|
||||
$deleted = ProductComponent::where('tenant_id', $tenantId)
|
||||
->where('parent_product_id', $productId)
|
||||
->delete();
|
||||
|
||||
$inserted = 0;
|
||||
foreach (array_chunk($rows, 500) as $chunk) {
|
||||
$ok =ProductComponent::insert($chunk);
|
||||
$inserted += $ok ? count($chunk) : 0;
|
||||
}
|
||||
|
||||
return [
|
||||
'deleted_count' => $deleted,
|
||||
'inserted_count' => $inserted,
|
||||
'message' => 'BOM 저장 성공',
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/** 제품별: 현재 BOM에 쓰인 카테고리 */
|
||||
public function listCategoriesForProduct(int $productId): array
|
||||
{
|
||||
if ($productId <= 0) throw new BadRequestHttpException(__('error.bad_request'));
|
||||
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$rows = ProductComponent::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('parent_product_id', $productId)
|
||||
->whereNotNull('category_name')
|
||||
->select([
|
||||
DB::raw('category_id'),
|
||||
DB::raw('category_name'),
|
||||
DB::raw('COUNT(*) as count'),
|
||||
])
|
||||
->groupBy('category_id', 'category_name')
|
||||
->orderByDesc('count')
|
||||
->orderBy('category_name')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/** 테넌트 전역: 자주 쓰인 카테고리 추천(+검색) */
|
||||
public function listCategoriesForTenant(?string $q, int $limit = 20): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = ProductComponent::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereNotNull('category_name')
|
||||
->select([
|
||||
DB::raw('category_id'),
|
||||
DB::raw('category_name'),
|
||||
DB::raw('COUNT(*) as count'),
|
||||
])
|
||||
->groupBy('category_id', 'category_name');
|
||||
|
||||
if ($q) {
|
||||
$query->havingRaw('category_name LIKE ?', ["%{$q}%"]);
|
||||
}
|
||||
|
||||
$rows = $query
|
||||
->orderByDesc('count')
|
||||
->orderBy('category_name')
|
||||
->limit($limit > 0 ? $limit : 20)
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function tree($request, int $productId): array
|
||||
{
|
||||
$depth = (int) $request->query('depth', config('products.default_tree_depth', 10));
|
||||
$resolver = app(ProductComponentResolver::class);
|
||||
|
||||
// 트리 배열만 반환 (ApiResponse가 바깥에서 래핑)
|
||||
return $resolver->resolveTree($productId, $depth);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user