363 lines
11 KiB
PHP
363 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Constants\SystemFields;
|
|
use App\Models\Commons\Category;
|
|
use App\Models\ItemMaster\ItemField;
|
|
use App\Models\Products\CommonCode;
|
|
use App\Models\Products\Product;
|
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
|
|
|
class ProductService extends Service
|
|
{
|
|
/**
|
|
* products 테이블의 고정 컬럼 목록 조회 (SystemFields + ItemField 기반)
|
|
*/
|
|
private function getKnownFields(): array
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
|
|
// 1. SystemFields에서 products 테이블 고정 컬럼
|
|
$systemFields = SystemFields::getReservedKeys(SystemFields::SOURCE_TABLE_PRODUCTS);
|
|
|
|
// 2. ItemField에서 storage_type='column'인 필드의 field_key 조회
|
|
$columnFields = ItemField::where('tenant_id', $tenantId)
|
|
->where('source_table', 'products')
|
|
->where('storage_type', 'column')
|
|
->whereNotNull('field_key')
|
|
->pluck('field_key')
|
|
->toArray();
|
|
|
|
// 3. 추가적인 API 전용 필드 (DB 컬럼이 아니지만 API에서 사용하는 필드)
|
|
$apiFields = ['item_type', 'type_code', 'bom'];
|
|
|
|
return array_unique(array_merge($systemFields, $columnFields, $apiFields));
|
|
}
|
|
|
|
/**
|
|
* 정의된 필드 외의 동적 필드를 options로 추출
|
|
*/
|
|
private function extractDynamicOptions(array $params): array
|
|
{
|
|
$knownFields = $this->getKnownFields();
|
|
|
|
$dynamicOptions = [];
|
|
foreach ($params as $key => $value) {
|
|
if (! in_array($key, $knownFields) && $value !== null && $value !== '') {
|
|
$dynamicOptions[$key] = $value;
|
|
}
|
|
}
|
|
|
|
return $dynamicOptions;
|
|
}
|
|
|
|
/**
|
|
* 기존 options 배열과 동적 필드를 병합
|
|
*/
|
|
private function mergeOptionsWithDynamic($existingOptions, array $dynamicOptions): array
|
|
{
|
|
if (! is_array($existingOptions) || empty($existingOptions)) {
|
|
return $dynamicOptions;
|
|
}
|
|
|
|
$isAssoc = array_keys($existingOptions) !== range(0, count($existingOptions) - 1);
|
|
|
|
if ($isAssoc) {
|
|
return array_merge($existingOptions, $dynamicOptions);
|
|
}
|
|
|
|
foreach ($dynamicOptions as $key => $value) {
|
|
$existingOptions[] = ['label' => $key, 'value' => $value];
|
|
}
|
|
|
|
return $existingOptions;
|
|
}
|
|
|
|
/**
|
|
* options 입력을 [{label, value, unit}] 형태로 정규화
|
|
*/
|
|
private function normalizeOptions(?array $in): ?array
|
|
{
|
|
if (! $in) {
|
|
return null;
|
|
}
|
|
|
|
$isAssoc = array_keys($in) !== range(0, count($in) - 1);
|
|
|
|
if ($isAssoc) {
|
|
$out = [];
|
|
foreach ($in as $k => $v) {
|
|
$label = trim((string) $k);
|
|
$value = is_scalar($v) ? trim((string) $v) : json_encode($v, JSON_UNESCAPED_UNICODE);
|
|
if ($label !== '' || $value !== '') {
|
|
$out[] = ['label' => $label, 'value' => $value, 'unit' => ''];
|
|
}
|
|
}
|
|
|
|
return $out ?: null;
|
|
}
|
|
|
|
$out = [];
|
|
foreach ($in as $a) {
|
|
if (! is_array($a)) {
|
|
continue;
|
|
}
|
|
$label = trim((string) ($a['label'] ?? ''));
|
|
$value = trim((string) ($a['value'] ?? ''));
|
|
$unit = trim((string) ($a['unit'] ?? ''));
|
|
|
|
if ($label === '' && $value === '') {
|
|
continue;
|
|
}
|
|
|
|
$out[] = ['label' => $label, 'value' => $value, 'unit' => $unit];
|
|
}
|
|
|
|
return $out ?: null;
|
|
}
|
|
|
|
/**
|
|
* 카테고리 트리 전체 조회 (parent_id = null 기준)
|
|
*/
|
|
public function getCategory($request)
|
|
{
|
|
$parentId = $request->parentId ?? null;
|
|
$group = $request->group ?? 'category';
|
|
|
|
// 재귀적으로 트리 구성
|
|
$list = $this->fetchCategoryTree($parentId, $group);
|
|
|
|
return $list;
|
|
}
|
|
|
|
/**
|
|
* 내부 재귀 함수 (하위 카테고리 트리 구조로 구성)
|
|
*/
|
|
protected function fetchCategoryTree(?int $parentId = null)
|
|
{
|
|
$tenantId = $this->tenantId(); // Base Service에서 상속받은 메서드
|
|
|
|
$query = Category::query()
|
|
->when($tenantId, fn ($q) => $q->where('tenant_id', $tenantId))
|
|
->when(
|
|
is_null($parentId),
|
|
fn ($q) => $q->whereNull('parent_id'),
|
|
fn ($q) => $q->where('parent_id', $parentId)
|
|
)
|
|
->where('is_active', 1)
|
|
->orderBy('sort_order');
|
|
|
|
$categories = $query->get();
|
|
|
|
foreach ($categories as $category) {
|
|
$children = $this->fetchCategoryTree($category->id);
|
|
$category->setRelation('children', $children);
|
|
}
|
|
|
|
return $categories;
|
|
}
|
|
|
|
/**
|
|
* (예시) 기존의 flat 리스트 조회
|
|
*/
|
|
public static function getCategoryFlat($group = 'category')
|
|
{
|
|
$query = CommonCode::where('code_group', $group)->whereNull('parent_id');
|
|
|
|
return $query->get();
|
|
}
|
|
|
|
// 목록/검색
|
|
public function index(array $params)
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
|
|
$size = (int) ($params['size'] ?? 20);
|
|
$q = trim((string) ($params['q'] ?? ''));
|
|
$categoryId = $params['category_id'] ?? null;
|
|
$productType = $params['product_type'] ?? null; // PRODUCT|PART|SUBASSEMBLY...
|
|
$active = $params['active'] ?? null; // 1/0
|
|
|
|
$query = Product::query()
|
|
->with('category:id,name') // 필요한 컬럼만 가져오기
|
|
->where('tenant_id', $tenantId);
|
|
|
|
if ($q !== '') {
|
|
$query->where(function ($w) use ($q) {
|
|
$w->where('name', 'like', "%{$q}%")
|
|
->orWhere('code', 'like', "%{$q}%")
|
|
->orWhere('description', 'like', "%{$q}%");
|
|
});
|
|
}
|
|
if ($categoryId) {
|
|
$query->where('category_id', (int) $categoryId);
|
|
}
|
|
if ($productType) {
|
|
$query->where('product_type', $productType);
|
|
}
|
|
// Note: is_active 필드는 하이브리드 구조로 전환하면서 제거됨
|
|
// 필요시 attributes JSON이나 별도 필드로 관리
|
|
// if ($active !== null && $active !== '') {
|
|
// $query->where('is_active', (int) $active);
|
|
// }
|
|
|
|
$paginator = $query->orderBy('id')->paginate($size);
|
|
|
|
// 날짜 형식을 위해 분리
|
|
$paginator->setCollection(
|
|
$paginator->getCollection()->transform(function ($item) {
|
|
$arr = $item->toArray();
|
|
$arr['created_at'] = $item->created_at
|
|
? $item->created_at->format('Y-m-d')
|
|
: null;
|
|
|
|
return $arr;
|
|
})
|
|
);
|
|
|
|
return $paginator;
|
|
}
|
|
|
|
// 생성
|
|
public function store(array $data)
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
$userId = $this->apiUserId();
|
|
|
|
// 동적 필드를 options에 병합
|
|
$dynamicOptions = $this->extractDynamicOptions($data);
|
|
if (! empty($dynamicOptions)) {
|
|
$existingOptions = $data['options'] ?? [];
|
|
$data['options'] = $this->mergeOptionsWithDynamic($existingOptions, $dynamicOptions);
|
|
}
|
|
|
|
// options 정규화
|
|
if (isset($data['options'])) {
|
|
$data['options'] = $this->normalizeOptions($data['options']);
|
|
}
|
|
|
|
$payload = $data;
|
|
|
|
// tenant별 code 유니크 수동 체크
|
|
$dup = Product::query()
|
|
->where('tenant_id', $tenantId)
|
|
->where('code', $payload['code'])
|
|
->exists();
|
|
if ($dup) {
|
|
throw new BadRequestHttpException(__('error.duplicate_key'));
|
|
}
|
|
|
|
// 기본값 설정
|
|
$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'] ?? true;
|
|
|
|
return Product::create($payload);
|
|
}
|
|
|
|
// 단건
|
|
public function show(int $id)
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
$p = Product::query()->where('tenant_id', $tenantId)->find($id);
|
|
if (! $p) {
|
|
throw new BadRequestHttpException(__('error.not_found'));
|
|
}
|
|
|
|
return $p;
|
|
}
|
|
|
|
// 수정
|
|
public function update(int $id, array $data)
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
$userId = $this->apiUserId();
|
|
|
|
$p = Product::query()->where('tenant_id', $tenantId)->find($id);
|
|
if (! $p) {
|
|
throw new BadRequestHttpException(__('error.not_found'));
|
|
}
|
|
|
|
// 동적 필드를 options에 병합
|
|
$dynamicOptions = $this->extractDynamicOptions($data);
|
|
if (! empty($dynamicOptions)) {
|
|
$existingOptions = $data['options'] ?? [];
|
|
$data['options'] = $this->mergeOptionsWithDynamic($existingOptions, $dynamicOptions);
|
|
}
|
|
|
|
// options 정규화
|
|
if (isset($data['options'])) {
|
|
$data['options'] = $this->normalizeOptions($data['options']);
|
|
}
|
|
|
|
$payload = $data;
|
|
|
|
// code 변경 시 중복 체크
|
|
if (isset($payload['code']) && $payload['code'] !== $p->code) {
|
|
$dup = Product::query()
|
|
->where('tenant_id', $tenantId)
|
|
->where('code', $payload['code'])
|
|
->exists();
|
|
if ($dup) {
|
|
throw new BadRequestHttpException(__('error.duplicate_key'));
|
|
}
|
|
}
|
|
|
|
$payload['updated_by'] = $userId;
|
|
$p->update($payload);
|
|
|
|
return $p->refresh();
|
|
}
|
|
|
|
// 삭제(soft)
|
|
public function destroy(int $id): void
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
$p = Product::query()->where('tenant_id', $tenantId)->find($id);
|
|
if (! $p) {
|
|
throw new BadRequestHttpException(__('error.not_found'));
|
|
}
|
|
$p->delete();
|
|
}
|
|
|
|
// 간편 검색(모달/드롭다운)
|
|
public function search(array $params)
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
$q = trim((string) ($params['q'] ?? ''));
|
|
$lim = (int) ($params['limit'] ?? 20);
|
|
|
|
$qr = Product::query()->where('tenant_id', $tenantId);
|
|
if ($q !== '') {
|
|
$qr->where(function ($w) use ($q) {
|
|
$w->where('name', 'like', "%{$q}%")
|
|
->orWhere('code', 'like', "%{$q}%");
|
|
});
|
|
}
|
|
|
|
return $qr->orderBy('name')->limit($lim)->get(['id', 'code', 'name', 'product_type', 'category_id']);
|
|
}
|
|
|
|
// Note: toggle 메서드는 is_active 필드 제거로 인해 비활성화됨
|
|
// 필요시 attributes JSON이나 별도 필드로 구현
|
|
// public function toggle(int $id)
|
|
// {
|
|
// $tenantId = $this->tenantId();
|
|
// $userId = $this->apiUserId();
|
|
//
|
|
// $p = Product::query()->where('tenant_id', $tenantId)->find($id);
|
|
// if (! $p) {
|
|
// throw new BadRequestHttpException(__('error.not_found'));
|
|
// }
|
|
//
|
|
// $p->is_active = $p->is_active ? 0 : 1;
|
|
// $p->updated_by = $userId;
|
|
// $p->save();
|
|
//
|
|
// return ['id' => $p->id, 'is_active' => (int) $p->is_active];
|
|
// }
|
|
}
|