2026-02-19 20:16:45 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
|
|
|
use App\Models\Items\Item;
|
|
|
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
|
|
|
|
|
|
|
|
|
class ItemManagementService
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* 품목 목록 조회 (검색, 유형 필터, 페이지네이션)
|
|
|
|
|
*/
|
|
|
|
|
public function getItemList(array $filters): LengthAwarePaginator
|
|
|
|
|
{
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
$search = $filters['search'] ?? null;
|
|
|
|
|
$itemType = $filters['item_type'] ?? null;
|
|
|
|
|
$perPage = $filters['per_page'] ?? 50;
|
|
|
|
|
|
|
|
|
|
return Item::withoutGlobalScopes()
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->search($search)
|
|
|
|
|
->active()
|
|
|
|
|
->when($itemType, function ($query, $types) {
|
|
|
|
|
$typeList = explode(',', $types);
|
|
|
|
|
$query->whereIn('item_type', $typeList);
|
|
|
|
|
})
|
|
|
|
|
->orderBy('code')
|
|
|
|
|
->paginate($perPage);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* BOM 재귀 트리 조회
|
|
|
|
|
*/
|
|
|
|
|
public function getBomTree(int $itemId, int $maxDepth = 10): array
|
|
|
|
|
{
|
|
|
|
|
$item = Item::withoutGlobalScopes()
|
|
|
|
|
->where('tenant_id', session('selected_tenant_id'))
|
|
|
|
|
->with('details')
|
|
|
|
|
->findOrFail($itemId);
|
|
|
|
|
|
|
|
|
|
return $this->buildBomNode($item, 0, $maxDepth, []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 품목 상세 조회 (1depth BOM + 파일 + 절곡정보)
|
|
|
|
|
*/
|
|
|
|
|
public function getItemDetail(int $itemId): array
|
|
|
|
|
{
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
|
|
|
|
$item = Item::withoutGlobalScopes()
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->with(['details', 'category', 'files'])
|
|
|
|
|
->findOrFail($itemId);
|
|
|
|
|
|
|
|
|
|
// BOM 1depth: 직접 연결된 자식 품목만
|
|
|
|
|
$bomChildren = [];
|
|
|
|
|
$bomData = $item->bom ?? [];
|
2026-02-25 11:45:01 +09:00
|
|
|
if (! empty($bomData)) {
|
2026-02-19 20:16:45 +09:00
|
|
|
$childIds = array_column($bomData, 'child_item_id');
|
|
|
|
|
$children = Item::withoutGlobalScopes()
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->whereIn('id', $childIds)
|
|
|
|
|
->get(['id', 'code', 'name', 'item_type', 'unit'])
|
|
|
|
|
->keyBy('id');
|
|
|
|
|
|
|
|
|
|
foreach ($bomData as $bom) {
|
|
|
|
|
$child = $children->get($bom['child_item_id']);
|
|
|
|
|
if ($child) {
|
|
|
|
|
$bomChildren[] = [
|
|
|
|
|
'id' => $child->id,
|
|
|
|
|
'code' => $child->code,
|
|
|
|
|
'name' => $child->name,
|
|
|
|
|
'item_type' => $child->item_type,
|
|
|
|
|
'unit' => $child->unit,
|
|
|
|
|
'quantity' => $bom['quantity'] ?? 1,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'item' => $item,
|
|
|
|
|
'bom_children' => $bomChildren,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Private ──
|
|
|
|
|
|
|
|
|
|
private function buildBomNode(Item $item, int $depth, int $maxDepth, array $visited): array
|
|
|
|
|
{
|
|
|
|
|
// 순환 참조 방지: visited 배열 + maxDepth 이중 안전장치
|
|
|
|
|
if (in_array($item->id, $visited) || $depth >= $maxDepth) {
|
|
|
|
|
return $this->formatNode($item, $depth, []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$visited[] = $item->id;
|
|
|
|
|
$children = [];
|
|
|
|
|
|
|
|
|
|
$bomData = $item->bom ?? [];
|
2026-02-25 11:45:01 +09:00
|
|
|
if (! empty($bomData)) {
|
2026-02-19 20:16:45 +09:00
|
|
|
$childIds = array_column($bomData, 'child_item_id');
|
|
|
|
|
$childItems = Item::withoutGlobalScopes()
|
|
|
|
|
->where('tenant_id', session('selected_tenant_id'))
|
|
|
|
|
->whereIn('id', $childIds)
|
|
|
|
|
->get()
|
|
|
|
|
->keyBy('id');
|
|
|
|
|
|
|
|
|
|
foreach ($bomData as $bom) {
|
|
|
|
|
$childItem = $childItems->get($bom['child_item_id']);
|
|
|
|
|
if ($childItem) {
|
|
|
|
|
$childNode = $this->buildBomNode($childItem, $depth + 1, $maxDepth, $visited);
|
|
|
|
|
$childNode['quantity'] = $bom['quantity'] ?? 1;
|
|
|
|
|
$children[] = $childNode;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->formatNode($item, $depth, $children);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function formatNode(Item $item, int $depth, array $children): array
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
'id' => $item->id,
|
|
|
|
|
'code' => $item->code,
|
|
|
|
|
'name' => $item->name,
|
|
|
|
|
'item_type' => $item->item_type,
|
|
|
|
|
'unit' => $item->unit,
|
|
|
|
|
'depth' => $depth,
|
|
|
|
|
'has_children' => count($children) > 0,
|
|
|
|
|
'children' => $children,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|