Files
sam-api/app/Services/ProductService.php
hskwon e848e12412 feat: Product API 하이브리드 구조 지원 (Phase 1)
[FormRequest 업데이트]
- ProductStoreRequest, ProductUpdateRequest에 하이브리드 필드 추가
  - 고정 필드: safety_stock, lead_time, is_variable_size, product_category, part_type
  - 동적 필드: attributes, attributes_archive
  - unit 필드 추가
- is_active → 제거 (모델과 일치)
- boolean 검증 개선 (in:0,1 → boolean)

[ProductService 업데이트]
- store/update 메서드 중복 Validator 제거 (FormRequest가 검증)
- 기본값 설정 간소화 (true/false로 통일)
- is_active 관련 로직 주석처리
  - index 메서드 필터
  - toggle 메서드 비활성화
  - search 메서드 select 컬럼 수정
- Validator import 제거

[ProductController 업데이트]
- toggle 메서드 주석처리 (is_active 제거에 따름)

[기능 변경]
- 기존: 고정 필드 위주
- 변경: 하이브리드 구조 (최소 고정 + attributes JSON)
- attributes를 통해 테넌트별 커스텀 필드 지원

[비고]
- is_active는 필요시 attributes JSON이나 별도 필드로 재구현 가능
- toggle 기능도 필요시 복원 가능 (주석으로 보존)
2025-11-14 12:42:08 +09:00

233 lines
7.0 KiB
PHP

<?php
namespace App\Services;
use App\Models\Commons\Category;
use App\Models\Products\CommonCode;
use App\Models\Products\Product;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class ProductService extends Service
{
/**
* 카테고리 트리 전체 조회 (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();
// FormRequest에서 이미 검증됨
$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'));
}
// FormRequest에서 이미 검증됨
$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];
// }
}