fix : 카테고리, 제품등록, BOM등록 API (일부 개발 - BOM 추가 작업 필요)

This commit is contained in:
2025-08-25 17:46:34 +09:00
parent 6307fdc1dc
commit 52bf8527e2
16 changed files with 2591 additions and 80 deletions

View File

@@ -0,0 +1,246 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use App\Models\Commons\CategoryField; // 가정: Eloquent 모델 경로
use App\Models\Commons\Category;
class CategoryFieldService extends Service
{
public function index(int $categoryId, array $params)
{
$tenantId = $this->tenantId();
$size = (int)($params['size'] ?? 20);
$sort = $params['sort'] ?? 'sort_order';
$order = strtolower($params['order'] ?? 'asc') === 'desc' ? 'desc' : 'asc';
return CategoryField::query()
->where('tenant_id', $tenantId)
->where('category_id', $categoryId)
->orderBy($sort, $order)
->paginate($size);
}
public function store(int $categoryId, array $data)
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$this->assertCategoryExists($tenantId, $categoryId);
$v = Validator::make($data, [
'field_key' => 'required|string|max:30|alpha_dash',
'field_name' => 'required|string|max:100',
'field_type' => 'required|string|max:20',
'is_required' => 'nullable|in:Y,N',
'sort_order' => 'nullable|integer|min:0',
'default_value' => 'nullable|string|max:100',
'options' => 'nullable|json',
'description' => 'nullable|string|max:255',
]);
$payload = $v->validate();
// 카테고리 내 field_key 유니크 검증
$exists = CategoryField::query()
->where(compact('tenant_id'))
->where('category_id', $categoryId)
->where('field_key', $payload['field_key'])
->exists();
if ($exists) {
throw new BadRequestHttpException(__('error.duplicate_key')); // ko/error.php에 매핑
}
$payload['tenant_id'] = $tenantId;
$payload['category_id'] = $categoryId;
$payload['is_required'] = $payload['is_required'] ?? 'N';
$payload['sort_order'] = $payload['sort_order'] ?? 0;
$payload['created_by'] = $userId;
return CategoryField::create($payload);
}
public function show(int $fieldId)
{
$tenantId = $this->tenantId();
$field = CategoryField::query()
->where('tenant_id', $tenantId)
->find($fieldId);
if (!$field) {
throw new BadRequestHttpException(__('error.not_found'));
}
return $field;
}
public function update(int $fieldId, array $data)
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$field = CategoryField::query()
->where('tenant_id', $tenantId)
->find($fieldId);
if (!$field) {
throw new BadRequestHttpException(__('error.not_found'));
}
$v = Validator::make($data, [
'field_key' => 'sometimes|string|max:30|alpha_dash',
'field_name' => 'sometimes|string|max:100',
'field_type' => 'sometimes|string|max:20',
'is_required' => 'sometimes|in:Y,N',
'sort_order' => 'sometimes|integer|min:0',
'default_value' => 'nullable|string|max:100',
'options' => 'nullable|json',
'description' => 'nullable|string|max:255',
]);
$payload = $v->validate();
if (isset($payload['field_key']) && $payload['field_key'] !== $field->field_key) {
$dup = CategoryField::query()
->where('tenant_id', $tenantId)
->where('category_id', $field->category_id)
->where('field_key', $payload['field_key'])
->exists();
if ($dup) {
throw new BadRequestHttpException(__('error.duplicate_key'));
}
}
$payload['updated_by'] = $userId;
$field->update($payload);
return $field->refresh();
}
public function destroy(int $fieldId): void
{
$tenantId = $this->tenantId();
$field = CategoryField::query()
->where('tenant_id', $tenantId)
->find($fieldId);
if (!$field) {
throw new BadRequestHttpException(__('error.not_found'));
}
$field->delete();
}
public function reorder(int $categoryId, array $items): void
{
$tenantId = $this->tenantId();
$this->assertCategoryExists($tenantId, $categoryId);
$rows = $items['items'] ?? $items; // 둘 다 허용
if (!is_array($rows)) {
throw new BadRequestHttpException(__('error.invalid_payload'));
}
DB::transaction(function () use ($tenantId, $categoryId, $rows) {
foreach ($rows as $row) {
if (!isset($row['id'], $row['sort_order'])) continue;
CategoryField::query()
->where('tenant_id', $tenantId)
->where('category_id', $categoryId)
->where('id', $row['id'])
->update(['sort_order' => (int)$row['sort_order']]);
}
});
}
public function bulkUpsert(int $categoryId, array $items): array
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$this->assertCategoryExists($tenantId, $categoryId);
if (!is_array($items) || empty($items)) {
throw new BadRequestHttpException(__('error.empty_items'));
}
$result = ['created' => 0, 'updated' => 0];
DB::transaction(function () use ($tenantId, $userId, $categoryId, $items, &$result) {
foreach ($items as $it) {
$v = Validator::make($it, [
'id' => 'nullable|integer',
'field_key' => 'sometimes|required_without:id|string|max:30|alpha_dash',
'field_name' => 'required|string|max:100',
'field_type' => 'required|string|max:20',
'is_required' => 'nullable|in:Y,N',
'sort_order' => 'nullable|integer|min:0',
'default_value' => 'nullable|string|max:100',
'options' => 'nullable|json',
'description' => 'nullable|string|max:255',
]);
$payload = $v->validate();
if (!empty($payload['id'])) {
$model = CategoryField::query()
->where('tenant_id', $tenantId)
->where('category_id', $categoryId)
->find($payload['id']);
if (!$model) {
throw new BadRequestHttpException(__('error.not_found'));
}
// field_key 변경 유니크 검사
if (isset($payload['field_key']) && $payload['field_key'] !== $model->field_key) {
$dup = CategoryField::query()
->where('tenant_id', $tenantId)
->where('category_id', $categoryId)
->where('field_key', $payload['field_key'])
->exists();
if ($dup) {
throw new BadRequestHttpException(__('error.duplicate_key'));
}
}
$payload['updated_by'] = $userId;
$model->update($payload);
$result['updated']++;
} else {
// 신규 생성
if (empty($payload['field_key'])) {
throw new BadRequestHttpException(__('error.required', ['attr' => 'field_key']));
}
$dup = CategoryField::query()
->where('tenant_id', $tenantId)
->where('category_id', $categoryId)
->where('field_key', $payload['field_key'])
->exists();
if ($dup) {
throw new BadRequestHttpException(__('error.duplicate_key'));
}
$payload['tenant_id'] = $tenantId;
$payload['category_id'] = $categoryId;
$payload['is_required'] = $payload['is_required'] ?? 'N';
$payload['sort_order'] = $payload['sort_order'] ?? 0;
$payload['created_by'] = $userId;
CategoryField::create($payload);
$result['created']++;
}
}
});
return $result;
}
private function assertCategoryExists(int $tenantId, int $categoryId): void
{
$exists = Category::query()
->where('tenant_id', $tenantId)
->where('id', $categoryId)
->exists();
if (!$exists) {
throw new BadRequestHttpException(__('error.category_not_found'));
}
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Services;
use Illuminate\Support\Carbon;
use App\Models\Commons\CategoryLog;
class CategoryLogService extends Service
{
public function index(int $categoryId, array $params)
{
$tenantId = $this->tenantId();
$size = (int)($params['size'] ?? 20);
$action = $params['action'] ?? null; // insert|update|delete
$from = $params['from'] ?? null; // Y-m-d
$to = $params['to'] ?? null;
$q = CategoryLog::query()
->where('tenant_id', $tenantId)
->where('category_id', $categoryId)
->orderByDesc('changed_at');
if ($action) $q->where('action', $action);
if ($from) $q->whereDate('changed_at', '>=', Carbon::parse($from)->toDateString());
if ($to) $q->whereDate('changed_at', '<=', Carbon::parse($to)->toDateString());
return $q->paginate($size);
}
public function show(int $logId)
{
$tenantId = $this->tenantId();
$log = CategoryLog::query()
->where('tenant_id', $tenantId)
->find($logId);
if (!$log) throw new BadRequestHttpException(__('error.not_found'));
return $log;
}
}

View File

@@ -0,0 +1,194 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use App\Models\Commons\CategoryTemplate;
use App\Models\Commons\Category;
use App\Models\Commons\CategoryField;
class CategoryTemplateService extends Service
{
public function index(int $categoryId, array $params)
{
$tenantId = $this->tenantId();
$size = (int)($params['size'] ?? 20);
return CategoryTemplate::query()
->where('tenant_id', $tenantId)
->where('category_id', $categoryId)
->orderByDesc('version_no')
->paginate($size);
}
public function store(int $categoryId, array $data)
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$this->assertCategoryExists($tenantId, $categoryId);
$v = Validator::make($data, [
'version_no' => 'required|integer|min:1',
'template_json' => 'required|json',
'applied_at' => 'required|date',
'remarks' => 'nullable|string|max:255',
]);
$payload = $v->validate();
$dup = CategoryTemplate::query()
->where('tenant_id', $tenantId)
->where('category_id', $categoryId)
->where('version_no', $payload['version_no'])
->exists();
if ($dup) {
throw new BadRequestHttpException(__('error.duplicate_key')); // version_no 중복
}
$payload['tenant_id'] = $tenantId;
$payload['category_id'] = $categoryId;
$payload['created_by'] = $userId;
return CategoryTemplate::create($payload);
}
public function show(int $tplId)
{
$tenantId = $this->tenantId();
$tpl = CategoryTemplate::query()
->where('tenant_id', $tenantId)
->find($tplId);
if (!$tpl) throw new BadRequestHttpException(__('error.not_found'));
return $tpl;
}
public function update(int $tplId, array $data)
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$tpl = CategoryTemplate::query()
->where('tenant_id', $tenantId)
->find($tplId);
if (!$tpl) throw new BadRequestHttpException(__('error.not_found'));
$v = Validator::make($data, [
'template_json' => 'nullable|json',
'applied_at' => 'nullable|date',
'remarks' => 'nullable|string|max:255',
]);
$payload = $v->validate();
$payload['updated_by'] = $userId;
$tpl->update($payload);
return $tpl->refresh();
}
public function destroy(int $tplId): void
{
$tenantId = $this->tenantId();
$tpl = CategoryTemplate::query()
->where('tenant_id', $tenantId)
->find($tplId);
if (!$tpl) throw new BadRequestHttpException(__('error.not_found'));
$tpl->delete();
}
/**
* 적용 정책:
* - categories.active_template_version (또는 별도 맵 테이블)에 version_no 반영
* - (옵션) template_json 기반으로 category_fields를 실제로 갱신하려면 여기서 동기화
*/
public function apply(int $categoryId, int $tplId): void
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$tpl = CategoryTemplate::query()
->where('tenant_id', $tenantId)
->where('category_id', $categoryId)
->find($tplId);
if (!$tpl) throw new BadRequestHttpException(__('error.not_found'));
DB::transaction(function () use ($tenantId, $userId, $categoryId, $tpl) {
// 1) categories 테이블에 활성 버전 반영(컬럼이 있다면)
// Category::where('tenant_id', $tenantId)->where('id', $categoryId)->update([
// 'active_template_version' => $tpl->version_no,
// 'updated_by' => $userId,
// ]);
// 2) (선택) template_json → category_fields 동기화
// - 추가/수정/삭제 전략은 운영정책에 맞게 구현
// - 여기서는 예시로 "fields" 배열만 처리
// $snapshot = json_decode($tpl->template_json, true);
// foreach (($snapshot['fields'] ?? []) as $i => $f) {
// // key, name, type, required, default, options 매핑 ...
// }
});
}
public function preview(int $categoryId, int $tplId): array
{
$tenantId = $this->tenantId();
$tpl = CategoryTemplate::query()
->where('tenant_id', $tenantId)
->where('category_id', $categoryId)
->find($tplId);
if (!$tpl) throw new BadRequestHttpException(__('error.not_found'));
$json = json_decode($tpl->template_json, true);
if (!is_array($json)) {
throw new BadRequestHttpException(__('error.invalid_payload'));
}
// 프론트 렌더링 편의 구조로 가공 가능
return $json;
}
public function diff(int $categoryId, int $a, int $b): array
{
$tenantId = $this->tenantId();
$aTpl = CategoryTemplate::query()
->where('tenant_id', $tenantId)->where('category_id', $categoryId)
->where('version_no', $a)->first();
$bTpl = CategoryTemplate::query()
->where('tenant_id', $tenantId)->where('category_id', $categoryId)
->where('version_no', $b)->first();
if (!$aTpl || !$bTpl) throw new BadRequestHttpException(__('error.not_found'));
$aj = json_decode($aTpl->template_json, true) ?: [];
$bj = json_decode($bTpl->template_json, true) ?: [];
// 아주 단순한 diff 예시 (fields 키만 비교)
$aKeys = collect($aj['fields'] ?? [])->pluck('key')->all();
$bKeys = collect($bj['fields'] ?? [])->pluck('key')->all();
return [
'added' => array_values(array_diff($bKeys, $aKeys)),
'removed' => array_values(array_diff($aKeys, $bKeys)),
// 변경(diff in detail)은 정책에 맞게 확장
];
}
private function assertCategoryExists(int $tenantId, int $categoryId): void
{
$exists = Category::query()
->where('tenant_id', $tenantId)
->where('id', $categoryId)
->exists();
if (!$exists) {
throw new BadRequestHttpException(__('error.category_not_found'));
}
}
}

View File

@@ -0,0 +1,335 @@
<?php
namespace App\Services;
use App\Models\Materials\Material;
use App\Models\Products\Product;
use App\Models\Products\ProductComponent;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ProductBomService extends Service
{
/**
* 목록: 제품/자재를 통합 반환
* - 반환 형태 예:
* [
* { "id": 10, "ref_type": "PRODUCT", "ref_id": 3, "code": "P-003", "name": "모듈A", "quantity": "2.0000", "sort_order": 1, "is_default": 1 },
* { "id": 11, "ref_type": "MATERIAL", "ref_id": 5, "code": "M-005", "name": "알루미늄판", "unit":"EA", "quantity": "4.0000", "sort_order": 2 }
* ]
*/
public function index(int $parentProductId, array $params)
{
$tenantId = $this->tenantId();
// 부모 제품 유효성
$this->assertProduct($tenantId, $parentProductId);
$items = ProductComponent::query()
->where('tenant_id', $tenantId)
->where('parent_product_id', $parentProductId)
->orderBy('sort_order')
->get();
// 리졸브(제품/자재)
$productIds = $items->where('ref_type', 'PRODUCT')->pluck('child_product_id')->filter()->unique()->values();
$materialIds = $items->where('ref_type', 'MATERIAL')->pluck('material_id')->filter()->unique()->values();
$products = $productIds->isNotEmpty()
? Product::query()->where('tenant_id', $tenantId)->whereIn('id', $productIds)->get(['id','code','name','product_type','category_id'])->keyBy('id')
: collect();
$materials = $materialIds->isNotEmpty()
? Material::query()->where('tenant_id', $tenantId)->whereIn('id', $materialIds)->get(['id','material_code as code','name','unit','category_id'])->keyBy('id')
: collect();
return $items->map(function ($row) use ($products, $materials) {
$base = [
'id' => (int)$row->id,
'ref_type' => $row->ref_type,
'quantity' => $row->quantity,
'sort_order' => (int)$row->sort_order,
'is_default' => (int)$row->is_default,
];
if ($row->ref_type === 'PRODUCT') {
$p = $products->get($row->child_product_id);
return $base + [
'ref_id' => (int)$row->child_product_id,
'code' => $p?->code,
'name' => $p?->name,
'product_type' => $p?->product_type,
'category_id' => $p?->category_id,
];
} else { // MATERIAL
$m = $materials->get($row->material_id);
return $base + [
'ref_id' => (int)$row->material_id,
'code' => $m?->code,
'name' => $m?->name,
'unit' => $m?->unit,
'category_id' => $m?->category_id,
];
}
})->values();
}
/**
* 일괄 업서트
* items[]: { id?, ref_type: PRODUCT|MATERIAL, ref_id: int, quantity: number, sort_order?: int, is_default?: 0|1 }
*/
public function bulkUpsert(int $parentProductId, array $items): array
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$this->assertProduct($tenantId, $parentProductId);
if (!is_array($items) || empty($items)) {
throw new BadRequestHttpException(__('error.empty_items'));
}
$created = 0; $updated = 0;
DB::transaction(function () use ($tenantId, $userId, $parentProductId, $items, &$created, &$updated) {
foreach ($items as $it) {
$payload = $this->validateItem($it);
// ref 확인 & 자기참조 방지
$this->assertReference($tenantId, $parentProductId, $payload['ref_type'], (int)$payload['ref_id']);
if (!empty($it['id'])) {
$pc = ProductComponent::query()
->where('tenant_id', $tenantId)
->where('parent_product_id', $parentProductId)
->find((int)$it['id']);
if (!$pc) throw new BadRequestHttpException(__('error.not_found'));
// ref 변경 허용 시: 충돌 검사
[$childProductId, $materialId] = $this->splitRef($payload);
$pc->update([
'ref_type' => $payload['ref_type'],
'child_product_id' => $childProductId,
'material_id' => $materialId,
'quantity' => $payload['quantity'],
'sort_order' => $payload['sort_order'] ?? $pc->sort_order,
'is_default' => $payload['is_default'] ?? $pc->is_default,
'updated_by' => $userId,
]);
$updated++;
} else {
// 신규
[$childProductId, $materialId] = $this->splitRef($payload);
ProductComponent::create([
'tenant_id' => $tenantId,
'parent_product_id' => $parentProductId,
'ref_type' => $payload['ref_type'],
'child_product_id' => $childProductId,
'material_id' => $materialId,
'quantity' => $payload['quantity'],
'sort_order' => $payload['sort_order'] ?? 0,
'is_default' => $payload['is_default'] ?? 0,
'created_by' => $userId,
]);
$created++;
}
}
});
return compact('created', 'updated');
}
// 단건 수정
public function update(int $parentProductId, int $itemId, array $data)
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$this->assertProduct($tenantId, $parentProductId);
$pc = ProductComponent::query()
->where('tenant_id', $tenantId)
->where('parent_product_id', $parentProductId)
->find($itemId);
if (!$pc) throw new BadRequestHttpException(__('error.not_found'));
$v = Validator::make($data, [
'ref_type' => 'sometimes|in:PRODUCT,MATERIAL',
'ref_id' => 'sometimes|integer',
'quantity' => 'sometimes|numeric|min:0.0001',
'sort_order' => 'sometimes|integer|min:0',
'is_default' => 'sometimes|in:0,1',
]);
$payload = $v->validate();
if (isset($payload['ref_type']) || isset($payload['ref_id'])) {
$refType = $payload['ref_type'] ?? $pc->ref_type;
$refId = isset($payload['ref_id'])
? (int)$payload['ref_id']
: ($pc->ref_type === 'PRODUCT' ? (int)$pc->child_product_id : (int)$pc->material_id);
$this->assertReference($tenantId, $parentProductId, $refType, $refId);
[$childProductId, $materialId] = $this->splitRef(['ref_type' => $refType, 'ref_id' => $refId]);
$pc->ref_type = $refType;
$pc->child_product_id = $childProductId;
$pc->material_id = $materialId;
}
if (isset($payload['quantity'])) $pc->quantity = $payload['quantity'];
if (isset($payload['sort_order'])) $pc->sort_order = $payload['sort_order'];
if (isset($payload['is_default'])) $pc->is_default = $payload['is_default'];
$pc->updated_by = $userId;
$pc->save();
return $pc->refresh();
}
// 삭제
public function destroy(int $parentProductId, int $itemId): void
{
$tenantId = $this->tenantId();
$this->assertProduct($tenantId, $parentProductId);
$pc = ProductComponent::query()
->where('tenant_id', $tenantId)
->where('parent_product_id', $parentProductId)
->find($itemId);
if (!$pc) throw new BadRequestHttpException(__('error.not_found'));
$pc->delete();
}
// 정렬 변경
public function reorder(int $parentProductId, array $items): void
{
$tenantId = $this->tenantId();
$this->assertProduct($tenantId, $parentProductId);
if (!is_array($items)) throw new BadRequestHttpException(__('error.invalid_payload'));
DB::transaction(function () use ($tenantId, $parentProductId, $items) {
foreach ($items as $row) {
if (!isset($row['id'], $row['sort_order'])) continue;
ProductComponent::query()
->where('tenant_id', $tenantId)
->where('parent_product_id', $parentProductId)
->where('id', (int)$row['id'])
->update(['sort_order' => (int)$row['sort_order']]);
}
});
}
// 요약(간단 합계/건수)
public function summary(int $parentProductId): array
{
$tenantId = $this->tenantId();
$this->assertProduct($tenantId, $parentProductId);
$items = ProductComponent::query()
->where('tenant_id', $tenantId)
->where('parent_product_id', $parentProductId)
->get();
$cnt = $items->count();
$cntP = $items->where('ref_type','PRODUCT')->count();
$cntM = $items->where('ref_type','MATERIAL')->count();
$qtySum = (string)$items->sum('quantity');
return [
'count' => $cnt,
'count_product' => $cntP,
'count_material'=> $cntM,
'quantity_sum' => $qtySum,
];
}
// 유효성 검사(중복/자기참조/음수 등)
public function validateBom(int $parentProductId): array
{
$tenantId = $this->tenantId();
$this->assertProduct($tenantId, $parentProductId);
$items = ProductComponent::query()
->where('tenant_id', $tenantId)
->where('parent_product_id', $parentProductId)
->orderBy('sort_order')
->get();
$errors = [];
$seen = [];
foreach ($items as $row) {
if ($row->quantity <= 0) {
$errors[] = ['id' => $row->id, 'error' => 'INVALID_QUANTITY'];
}
$key = $row->ref_type . ':' . ($row->ref_type === 'PRODUCT' ? $row->child_product_id : $row->material_id);
if (isset($seen[$key])) {
$errors[] = ['id' => $row->id, 'error' => 'DUPLICATE_ITEM'];
} else {
$seen[$key] = true;
}
// 자기참조
if ($row->ref_type === 'PRODUCT' && (int)$row->child_product_id === (int)$parentProductId) {
$errors[] = ['id' => $row->id, 'error' => 'SELF_REFERENCE'];
}
}
return [
'valid' => count($errors) === 0,
'errors' => $errors,
];
}
// ---------- helpers ----------
private function validateItem(array $it): array
{
$v = Validator::make($it, [
'id' => 'nullable|integer',
'ref_type' => 'required|in:PRODUCT,MATERIAL',
'ref_id' => 'required|integer',
'quantity' => 'required|numeric|min:0.0001',
'sort_order' => 'nullable|integer|min:0',
'is_default' => 'nullable|in:0,1',
]);
return $v->validate();
}
private function splitRef(array $payload): array
{
// returns [child_product_id, material_id]
if ($payload['ref_type'] === 'PRODUCT') {
return [(int)$payload['ref_id'], null];
}
return [null, (int)$payload['ref_id']];
}
private function assertProduct(int $tenantId, int $productId): void
{
$exists = Product::query()->where('tenant_id', $tenantId)->where('id', $productId)->exists();
if (!$exists) {
// ko: 제품 정보를 찾을 수 없습니다.
throw new NotFoundHttpException(__('error.not_found_resource', ['resource' => '제품']));
}
}
private function assertReference(int $tenantId, int $parentProductId, string $refType, int $refId): void
{
if ($refType === 'PRODUCT') {
if ($refId === $parentProductId) {
throw new BadRequestHttpException(__('error.invalid_payload')); // 자기참조 방지
}
$ok = Product::query()->where('tenant_id', $tenantId)->where('id', $refId)->exists();
if (!$ok) throw new BadRequestHttpException(__('error.not_found'));
} else {
$ok = Material::query()->where('tenant_id', $tenantId)->where('id', $refId)->exists();
if (!$ok) throw new BadRequestHttpException(__('error.not_found'));
}
}
}

View File

@@ -3,8 +3,12 @@
namespace App\Services;
use App\Models\Products\CommonCode;
use App\Models\Products\Product;
use App\Models\Commons\Category;
use Illuminate\Support\Facades\Validator;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class ProductService
class ProductService extends Service
{
/**
@@ -24,16 +28,27 @@ public static function getCategory($request)
/**
* 내부 재귀 함수 (하위 카테고리 트리 구조로 구성)
*/
protected static function fetchCategoryTree($parentId = null, $group = 'category')
protected function fetchCategoryTree(?int $parentId = null)
{
$categories = CommonCode::where('code_group', 'category')
->where('parent_id', $parentId)
->orderBy('sort_order')->debug();
$categories = $categories->get();
$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) {
$category->children = self::fetchCategoryTree($category->id);
$children = $this->fetchCategoryTree($category->id);
$category->setRelation('children', $children);
}
return $categories;
}
@@ -46,5 +61,157 @@ public static function getCategoryFlat($group = 'category')
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()->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);
if ($active !== null && $active !== '') $query->where('is_active', (int)$active);
return $query->orderByDesc('id')->paginate($size);
}
// 생성
public function store(array $data)
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$v = Validator::make($data, [
'code' => 'required|string|max:30',
'name' => 'required|string|max:100',
'category_id' => 'required|integer',
'product_type' => 'required|string|max:30',
'attributes' => 'nullable|array',
'description' => 'nullable|string|max:255',
'is_sellable' => 'nullable|in:0,1',
'is_purchasable' => 'nullable|in:0,1',
'is_producible' => 'nullable|in:0,1',
'is_active' => 'nullable|in:0,1',
]);
$payload = $v->validate();
// tenant별 code 유니크 수동 체크(운영 전 DB 유니크 구성도 권장)
$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'] ?? 1;
$payload['is_purchasable'] = $payload['is_purchasable'] ?? 0;
$payload['is_producible'] = $payload['is_producible'] ?? 1;
$payload['is_active'] = $payload['is_active'] ?? 1;
// attributes array → json 저장 (Eloquent casts가 array면 그대로 가능)
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'));
$v = Validator::make($data, [
'code' => 'sometimes|string|max:30',
'name' => 'sometimes|string|max:100',
'category_id' => 'sometimes|integer',
'product_type' => 'sometimes|string|max:30',
'attributes' => 'nullable|array',
'description' => 'nullable|string|max:255',
'is_sellable' => 'nullable|in:0,1',
'is_purchasable' => 'nullable|in:0,1',
'is_producible' => 'nullable|in:0,1',
'is_active' => 'nullable|in:0,1',
]);
$payload = $v->validate();
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','is_active']);
}
// 활성 토글
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];
}
}