Files
sam-api/app/Services/ItemsService.php

364 lines
11 KiB
PHP
Raw Normal View History

<?php
namespace App\Services;
use App\Models\Materials\Material;
use App\Models\Products\Product;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
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,
];
}
/**
* 품목 생성 (Product 전용)
*
* @param array $data 검증된 데이터
*/
public function createItem(array $data): Product
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
// 품목 코드 중복 체크
$exists = Product::query()
->where('tenant_id', $tenantId)
->where('code', $data['code'])
->exists();
if ($exists) {
throw new BadRequestHttpException(__('error.duplicate_code'));
}
$payload = $data;
$payload['tenant_id'] = $tenantId;
$payload['created_by'] = $userId;
$payload['is_sellable'] = $payload['is_sellable'] ?? true;
$payload['is_purchasable'] = $payload['is_purchasable'] ?? false;
$payload['is_producible'] = $payload['is_producible'] ?? false;
return Product::create($payload);
}
/**
* 품목 수정 (Product 전용)
*
* @param int $id 품목 ID
* @param array $data 검증된 데이터
*/
public function updateItem(int $id, array $data): Product
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$product = Product::query()
->where('tenant_id', $tenantId)
->find($id);
if (! $product) {
throw new NotFoundHttpException(__('error.not_found'));
}
// 코드 변경 시 중복 체크
if (isset($data['code']) && $data['code'] !== $product->code) {
$exists = Product::query()
->where('tenant_id', $tenantId)
->where('code', $data['code'])
->where('id', '!=', $product->id)
->exists();
if ($exists) {
throw new BadRequestHttpException(__('error.duplicate_code'));
}
}
$data['updated_by'] = $userId;
$product->update($data);
return $product->refresh();
}
/**
* 품목 삭제 (Product 전용, Soft Delete)
*
* @param int $id 품목 ID
*/
public function deleteItem(int $id): void
{
$tenantId = $this->tenantId();
$product = Product::query()
->where('tenant_id', $tenantId)
->find($id);
if (! $product) {
throw new NotFoundHttpException(__('error.not_found'));
}
$product->delete();
}
/**
* 품목 일괄 삭제 (Product 전용, Soft Delete)
*
* @param array $ids 품목 ID 배열
*/
public function batchDeleteItems(array $ids): void
{
$tenantId = $this->tenantId();
$products = Product::query()
->where('tenant_id', $tenantId)
->whereIn('id', $ids)
->get();
if ($products->isEmpty()) {
throw new NotFoundHttpException(__('error.not_found'));
}
foreach ($products as $product) {
$product->delete();
}
}
/**
* 품목 상세 조회 (code 기반, BOM 포함 옵션)
*
* @param string $code 품목 코드
* @param bool $includeBom BOM 포함 여부
*/
public function getItemByCode(string $code, bool $includeBom = false): Product
{
$tenantId = $this->tenantId();
$query = Product::query()
->with('category:id,name')
->where('tenant_id', $tenantId)
->where('code', $code);
if ($includeBom) {
$query->with('componentLines.childProduct:id,code,name,unit');
}
$product = $query->first();
if (! $product) {
throw new NotFoundHttpException(__('error.not_found'));
}
return $product;
}
}