Files
sam-api/app/Services/Products/ProductComponentResolver.php

188 lines
6.7 KiB
PHP
Raw Normal View History

<?php
namespace App\Services\Products;
use App\Models\Products\Product;
use App\Models\Products\ProductComponent;
use App\Services\Service;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class ProductComponentResolver extends Service
{
public function __construct(
protected ?int $tenantId = null
) {
// 주입값 없으면 Service::tenantId() 사용 (app('tenant_id')에서 끌어옴)
$this->tenantId = $this->tenantId ?? $this->tenantId();
}
/** 테넌트별로 "제품처럼 자식이 있을 수 있는" ref_type 목록 */
protected function productLikeTypes(): array
{
$all = config('products.product_like_types', []);
$byTenant = Arr::get($all, (string)$this->tenantId, null);
if (is_array($byTenant) && $byTenant) return $byTenant;
return Arr::get($all, '*', ['PRODUCT']);
}
/** 한 부모 ID에 달린 컴포넌트 라인들 로드 (메모이즈) */
protected function getLinesForParent(int $parentId): array
{
// 간단 메모이즈(요청 범위); 대량 호출 방지
static $memo = [];
if (array_key_exists($parentId, $memo)) {
return $memo[$parentId];
}
$rows = ProductComponent::where('tenant_id', $this->tenantId)
->where('parent_product_id', $parentId)
->orderBy('sort_order')->orderBy('id')
->get([
'id','tenant_id','parent_product_id',
'category_id','category_name',
'ref_type','ref_id','quantity','sort_order',
])
->map(fn($r) => $r->getAttributes()) // ✅ 핵심 수정
->all();
return $memo[$parentId] = $rows;
}
/** ref_type/ref_id 에 해당하는 노드의 "표시용 정보"를 로드 */
protected function resolveNodeInfo(string $refType, int $refId): array
{
if ($refType === 'PRODUCT') {
$p = Product::query()
->where('tenant_id', $this->tenantId)
->find($refId, ['id','code','name','product_type','category_id']);
if (!$p) return ['id'=>$refId, 'code'=>null, 'name'=>null, 'product_type'=>null, 'category_id'=>null];
return [
'id' => $p->id,
'code' => $p->code,
'name' => $p->name,
'product_type' => $p->product_type,
'category_id' => $p->category_id,
];
}
// MATERIAL 등 다른 타입은 여기서 분기 추가
// if ($refType === 'MATERIAL') {
// $m = DB::table('materials')
// ->where('tenant_id', $this->tenantId)
// ->where('id', $refId)
// ->first(['id','code','name','unit']);
// return $m ? ['id'=>$m->id,'code'=>$m->code,'name'=>$m->name,'unit'=>$m->unit] : ['id'=>$refId,'code'=>null,'name'=>null];
// }
return ['id'=>$refId, 'code'=>null, 'name'=>null];
}
/**
* 단일 제품을 루트로 트리를 생성 (재귀 / 사이클 방지 / 깊이 제한)
*
* @param int $productId 루트 제품 ID
* @param int|null $maxDepth 최대 깊이(루트=0). null 이면 config default
* @return array 트리 구조
*/
public function resolveTree(int $productId, ?int $maxDepth = null): array
{
$maxDepth = $maxDepth ?? (int)config('products.default_tree_depth', 10);
$root = Product::query()
->where('tenant_id', $this->tenantId)
->findOrFail($productId, ['id','code','name','product_type','category_id']);
$visited = []; // 사이클 방지용 (product id 기준)
$node = [
'type' => 'PRODUCT',
'id' => $root->id,
'code' => $root->code,
'name' => $root->name,
'product_type' => $root->product_type,
'category_id' => $root->category_id,
'quantity' => 1, // 루트는 수량 1로 간주
'category' => null, // 루트는 임의
'children' => [],
'depth' => 0,
];
$node['children'] = $this->resolveChildren($root->id, 0, $maxDepth, $visited);
return $node;
}
/**
* 하위 노드() 재귀 확장
* @param int $parentId
* @param int $depth
* @param int $maxDepth
* @param array $visited product-id 기준 사이클 방지
* @return array
*/
protected function resolveChildren(int $parentId, int $depth, int $maxDepth, array &$visited): array
{
// 깊이 제한
if ($depth >= $maxDepth) return [];
$lines = $this->getLinesForParent($parentId);
if (!$lines) return [];
$productLike = $this->productLikeTypes();
$children = [];
foreach ($lines as $line) {
$refType = (string)$line['ref_type'];
$refId = (int)$line['ref_id'];
$qty = (float)$line['quantity'];
if (!$refType || $refId <= 0) {
// 로그 남기고 스킵
// logger()->warning('Invalid component line', ['line' => $line]);
continue;
}
$info = $this->resolveNodeInfo($refType, $refId);
$child = [
'type' => $refType,
'id' => $info['id'] ?? $refId,
'code' => $info['code'] ?? null,
'name' => $info['name'] ?? null,
'product_type'=> $info['product_type'] ?? null,
'category_id' => $info['category_id'] ?? null,
'quantity' => $qty,
'category' => [
'id' => $line['category_id'],
'name' => $line['category_name'],
],
'sort_order' => (int)$line['sort_order'],
'children' => [],
'depth' => $depth + 1,
];
// 제품처럼 자식이 달릴 수 있는 타입이면 재귀
if (in_array($refType, $productLike, true)) {
// 사이클 방지: 같은 product id 재방문 금지
$pid = (int)$child['id'];
if ($pid > 0) {
if (isset($visited[$pid])) {
$child['cycle'] = true; // 표식만 남기고 children 안탐
} else {
$visited[$pid] = true;
$child['children'] = $this->resolveChildren($pid, $depth + 1, $maxDepth, $visited);
unset($visited[$pid]); // 백트래킹
}
}
}
$children[] = $child;
}
return $children;
}
}