- 통합 품목 조회 API (materials + products UNION) - ItemsService, ItemsController, Swagger 문서 생성 - 타입 필터링 (FG/PT/SM/RM/CS), 검색, 카테고리 지원 - Collection merge 방식으로 UNION 쿼리 안정화 - 품목-가격 통합 조회 - PricingService.getPriceByType() 추가 (SALE/PURCHASE 지원) - 단일 품목 조회 시 판매가/매입가 선택적 포함 - 고객그룹 가격 우선순위 적용 및 시계열 조회 - 자재 타입 명시적 관리 - materials.material_type 컬럼 추가 (SM/RM/CS) - 기존 데이터 344개 자동 변환 (RAW→RM, SUB→SM) - 인덱스 추가로 조회 성능 최적화 - DB 데이터 정규화 - products.product_type: 760개 정규화 (PRODUCT→FG, PART/SUBASSEMBLY→PT) - 타입 코드 표준화로 API 일관성 확보 최종 데이터: 제품 760개(FG 297, PT 463), 자재 344개(SM 215, RM 129)
223 lines
7.4 KiB
PHP
223 lines
7.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Materials\Material;
|
|
use App\Models\Products\Product;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
|
|
class ItemsService extends Service
|
|
{
|
|
/**
|
|
* 통합 품목 조회 (materials + products UNION)
|
|
*
|
|
* @param array $filters 필터 조건 (type, search, category_id, page, size)
|
|
* @param int $perPage 페이지당 항목 수
|
|
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
|
*/
|
|
public function getItems(array $filters = [], int $perPage = 20)
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
|
|
// 필터 파라미터 추출
|
|
$types = $filters['type'] ?? ['FG', 'PT', 'SM', 'RM', 'CS'];
|
|
$search = trim($filters['search'] ?? $filters['q'] ?? '');
|
|
$categoryId = $filters['category_id'] ?? null;
|
|
|
|
// 타입을 배열로 변환 (문자열인 경우 쉼표로 분리)
|
|
if (is_string($types)) {
|
|
$types = array_map('trim', explode(',', $types));
|
|
}
|
|
|
|
// Product 타입 (FG, PT)
|
|
$productTypes = array_intersect(['FG', 'PT'], $types);
|
|
// Material 타입 (SM, RM, CS)
|
|
$materialTypes = array_intersect(['SM', 'RM', 'CS'], $types);
|
|
|
|
// Products 쿼리 (FG, PT 타입만)
|
|
$productsQuery = null;
|
|
if (! empty($productTypes)) {
|
|
$productsQuery = Product::query()
|
|
->where('tenant_id', $tenantId)
|
|
->whereIn('product_type', $productTypes)
|
|
->where('is_active', 1)
|
|
->select([
|
|
'id',
|
|
DB::raw("'PRODUCT' as item_type"),
|
|
'code',
|
|
'name',
|
|
'unit',
|
|
'category_id',
|
|
'product_type as type_code',
|
|
'created_at',
|
|
]);
|
|
|
|
// 검색 조건
|
|
if ($search !== '') {
|
|
$productsQuery->where(function ($q) use ($search) {
|
|
$q->where('name', 'like', "%{$search}%")
|
|
->orWhere('code', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// 카테고리 필터
|
|
if ($categoryId) {
|
|
$productsQuery->where('category_id', (int) $categoryId);
|
|
}
|
|
}
|
|
|
|
// Materials 쿼리 (SM, RM, CS 타입)
|
|
$materialsQuery = null;
|
|
if (! empty($materialTypes)) {
|
|
$materialsQuery = Material::query()
|
|
->where('tenant_id', $tenantId)
|
|
->whereIn('material_type', $materialTypes)
|
|
->select([
|
|
'id',
|
|
DB::raw("'MATERIAL' as item_type"),
|
|
'material_code as code',
|
|
'name',
|
|
'unit',
|
|
'category_id',
|
|
'material_type as type_code',
|
|
'created_at',
|
|
]);
|
|
|
|
// 검색 조건
|
|
if ($search !== '') {
|
|
$materialsQuery->where(function ($q) use ($search) {
|
|
$q->where('name', 'like', "%{$search}%")
|
|
->orWhere('item_name', 'like', "%{$search}%")
|
|
->orWhere('material_code', 'like', "%{$search}%")
|
|
->orWhere('search_tag', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// 카테고리 필터
|
|
if ($categoryId) {
|
|
$materialsQuery->where('category_id', (int) $categoryId);
|
|
}
|
|
}
|
|
|
|
// 각 쿼리 실행 후 merge (UNION 바인딩 문제 방지)
|
|
$items = collect([]);
|
|
|
|
if ($productsQuery) {
|
|
$products = $productsQuery->get();
|
|
$items = $items->merge($products);
|
|
}
|
|
|
|
if ($materialsQuery) {
|
|
$materials = $materialsQuery->get();
|
|
$items = $items->merge($materials);
|
|
}
|
|
|
|
// 정렬 (name 기준 오름차순)
|
|
$items = $items->sortBy('name')->values();
|
|
|
|
// 페이지네이션 처리
|
|
$page = request()->input('page', 1);
|
|
$offset = ($page - 1) * $perPage;
|
|
$total = $items->count();
|
|
|
|
$paginatedItems = $items->slice($offset, $perPage)->values();
|
|
|
|
return new \Illuminate\Pagination\LengthAwarePaginator(
|
|
$paginatedItems,
|
|
$total,
|
|
$perPage,
|
|
$page,
|
|
['path' => request()->url(), 'query' => request()->query()]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 단일 품목 조회
|
|
*
|
|
* @param string $itemType 'PRODUCT' | 'MATERIAL'
|
|
* @param int $id 품목 ID
|
|
* @param bool $includePrice 가격 정보 포함 여부
|
|
* @param int|null $clientId 고객 ID (가격 조회 시)
|
|
* @param string|null $priceDate 기준일 (가격 조회 시)
|
|
* @return array 품목 데이터 (item_type, prices 포함)
|
|
*/
|
|
public function getItem(
|
|
string $itemType,
|
|
int $id,
|
|
bool $includePrice = false,
|
|
?int $clientId = null,
|
|
?string $priceDate = null
|
|
): array {
|
|
$tenantId = $this->tenantId();
|
|
|
|
$itemType = strtoupper($itemType);
|
|
|
|
if ($itemType === 'PRODUCT') {
|
|
$product = Product::query()
|
|
->with('category:id,name')
|
|
->where('tenant_id', $tenantId)
|
|
->find($id);
|
|
|
|
if (! $product) {
|
|
throw new NotFoundHttpException(__('error.not_found'));
|
|
}
|
|
|
|
$data = $product->toArray();
|
|
$data['item_type'] = 'PRODUCT';
|
|
$data['type_code'] = $product->product_type;
|
|
|
|
// 가격 정보 추가
|
|
if ($includePrice) {
|
|
$data['prices'] = $this->fetchPrices($itemType, $id, $clientId, $priceDate);
|
|
}
|
|
|
|
return $data;
|
|
} elseif ($itemType === 'MATERIAL') {
|
|
$material = Material::query()
|
|
->where('tenant_id', $tenantId)
|
|
->find($id);
|
|
|
|
if (! $material) {
|
|
throw new NotFoundHttpException(__('error.not_found'));
|
|
}
|
|
|
|
$data = $material->toArray();
|
|
$data['item_type'] = 'MATERIAL';
|
|
$data['code'] = $material->material_code;
|
|
$data['type_code'] = $material->material_type;
|
|
|
|
// 가격 정보 추가
|
|
if ($includePrice) {
|
|
$data['prices'] = $this->fetchPrices($itemType, $id, $clientId, $priceDate);
|
|
}
|
|
|
|
return $data;
|
|
} else {
|
|
throw new \InvalidArgumentException(__('error.invalid_item_type'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 품목의 판매가/매입가 조회
|
|
*
|
|
* @param string $itemType 'PRODUCT' | 'MATERIAL'
|
|
* @param int $itemId 품목 ID
|
|
* @param int|null $clientId 고객 ID
|
|
* @param string|null $priceDate 기준일
|
|
* @return array ['sale' => array, 'purchase' => array]
|
|
*/
|
|
private function fetchPrices(string $itemType, int $itemId, ?int $clientId, ?string $priceDate): array
|
|
{
|
|
// PricingService DI가 없으므로 직접 생성
|
|
$pricingService = app(\App\Services\Pricing\PricingService::class);
|
|
|
|
$salePrice = $pricingService->getPriceByType($itemType, $itemId, 'SALE', $clientId, $priceDate);
|
|
$purchasePrice = $pricingService->getPriceByType($itemType, $itemId, 'PURCHASE', $clientId, $priceDate);
|
|
|
|
return [
|
|
'sale' => $salePrice,
|
|
'purchase' => $purchasePrice,
|
|
];
|
|
}
|
|
} |