- ItemsController 및 ItemsService CRUD 메서드 구현 - FormRequest 검증 클래스 추가 (ItemStoreRequest, ItemUpdateRequest) - Swagger 문서 완성 (ItemsApi.php) - 품목 생성/조회/수정/삭제 엔드포인트 추가 - i18n 메시지 키 추가 (message.item) - Code 기반 라우팅 적용 - Hybrid 구조 지원 (고정 필드 + attributes JSON)
343 lines
11 KiB
PHP
343 lines
11 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\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 string $code 품목 코드
|
|
* @param array $data 검증된 데이터
|
|
*/
|
|
public function updateItem(string $code, array $data): Product
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
$userId = $this->apiUserId();
|
|
|
|
$product = Product::query()
|
|
->where('tenant_id', $tenantId)
|
|
->where('code', $code)
|
|
->first();
|
|
|
|
if (! $product) {
|
|
throw new NotFoundHttpException(__('error.not_found'));
|
|
}
|
|
|
|
// 코드 변경 시 중복 체크
|
|
if (isset($data['code']) && $data['code'] !== $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 string $code 품목 코드
|
|
*/
|
|
public function deleteItem(string $code): void
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
|
|
$product = Product::query()
|
|
->where('tenant_id', $tenantId)
|
|
->where('code', $code)
|
|
->first();
|
|
|
|
if (! $product) {
|
|
throw new NotFoundHttpException(__('error.not_found'));
|
|
}
|
|
|
|
$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;
|
|
}
|
|
}
|