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 기능도 필요시 복원 가능 (주석으로 보존)
This commit is contained in:
@@ -70,11 +70,13 @@ public function search(Request $request)
|
||||
}, __('message.product.searched'));
|
||||
}
|
||||
|
||||
// Note: toggle 메서드는 is_active 필드 제거로 인해 비활성화됨
|
||||
// 필요시 attributes JSON이나 별도 필드로 구현
|
||||
// POST /products/{id}/toggle
|
||||
public function toggle(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->toggle($id);
|
||||
}, __('message.product.toggled'));
|
||||
}
|
||||
// public function toggle(int $id)
|
||||
// {
|
||||
// return ApiResponse::handle(function () use ($id) {
|
||||
// return $this->service->toggle($id);
|
||||
// }, __('message.product.toggled'));
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -14,16 +14,29 @@ public function authorize(): bool
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
// 기본 필드
|
||||
'code' => 'required|string|max:30',
|
||||
'name' => 'required|string|max:100',
|
||||
'unit' => 'nullable|string|max:10',
|
||||
'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',
|
||||
|
||||
// 상태 플래그
|
||||
'is_sellable' => 'nullable|boolean',
|
||||
'is_purchasable' => 'nullable|boolean',
|
||||
'is_producible' => 'nullable|boolean',
|
||||
|
||||
// 하이브리드 구조: 고정 필드
|
||||
'safety_stock' => 'nullable|integer|min:0',
|
||||
'lead_time' => 'nullable|integer|min:0',
|
||||
'is_variable_size' => 'nullable|boolean',
|
||||
'product_category' => 'nullable|string|max:20',
|
||||
'part_type' => 'nullable|string|max:20',
|
||||
|
||||
// 하이브리드 구조: 동적 필드
|
||||
'attributes' => 'nullable|array',
|
||||
'attributes_archive' => 'nullable|array',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,16 +14,29 @@ public function authorize(): bool
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
// 기본 필드
|
||||
'code' => 'sometimes|string|max:30',
|
||||
'name' => 'sometimes|string|max:100',
|
||||
'unit' => 'nullable|string|max:10',
|
||||
'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',
|
||||
|
||||
// 상태 플래그
|
||||
'is_sellable' => 'nullable|boolean',
|
||||
'is_purchasable' => 'nullable|boolean',
|
||||
'is_producible' => 'nullable|boolean',
|
||||
|
||||
// 하이브리드 구조: 고정 필드
|
||||
'safety_stock' => 'nullable|integer|min:0',
|
||||
'lead_time' => 'nullable|integer|min:0',
|
||||
'is_variable_size' => 'nullable|boolean',
|
||||
'product_category' => 'nullable|string|max:20',
|
||||
'part_type' => 'nullable|string|max:20',
|
||||
|
||||
// 하이브리드 구조: 동적 필드
|
||||
'attributes' => 'nullable|array',
|
||||
'attributes_archive' => 'nullable|array',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
use App\Models\Commons\Category;
|
||||
use App\Models\Products\CommonCode;
|
||||
use App\Models\Products\Product;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
class ProductService extends Service
|
||||
@@ -89,9 +88,11 @@ public function index(array $params)
|
||||
if ($productType) {
|
||||
$query->where('product_type', $productType);
|
||||
}
|
||||
if ($active !== null && $active !== '') {
|
||||
$query->where('is_active', (int) $active);
|
||||
}
|
||||
// Note: is_active 필드는 하이브리드 구조로 전환하면서 제거됨
|
||||
// 필요시 attributes JSON이나 별도 필드로 관리
|
||||
// if ($active !== null && $active !== '') {
|
||||
// $query->where('is_active', (int) $active);
|
||||
// }
|
||||
|
||||
$paginator = $query->orderBy('id')->paginate($size);
|
||||
|
||||
@@ -116,21 +117,10 @@ 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();
|
||||
// FormRequest에서 이미 검증됨
|
||||
$payload = $data;
|
||||
|
||||
// tenant별 code 유니크 수동 체크(운영 전 DB 유니크 구성도 권장)
|
||||
// tenant별 code 유니크 수동 체크
|
||||
$dup = Product::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $payload['code'])
|
||||
@@ -139,14 +129,13 @@ public function store(array $data)
|
||||
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;
|
||||
$payload['is_sellable'] = $payload['is_sellable'] ?? true;
|
||||
$payload['is_purchasable'] = $payload['is_purchasable'] ?? false;
|
||||
$payload['is_producible'] = $payload['is_producible'] ?? true;
|
||||
|
||||
// attributes array → json 저장 (Eloquent casts가 array면 그대로 가능)
|
||||
return Product::create($payload);
|
||||
}
|
||||
|
||||
@@ -173,20 +162,10 @@ public function update(int $id, array $data)
|
||||
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();
|
||||
// FormRequest에서 이미 검증됨
|
||||
$payload = $data;
|
||||
|
||||
// code 변경 시 중복 체크
|
||||
if (isset($payload['code']) && $payload['code'] !== $p->code) {
|
||||
$dup = Product::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
@@ -229,24 +208,25 @@ public function search(array $params)
|
||||
});
|
||||
}
|
||||
|
||||
return $qr->orderBy('name')->limit($lim)->get(['id', 'code', 'name', 'product_type', 'category_id', 'is_active']);
|
||||
return $qr->orderBy('name')->limit($lim)->get(['id', 'code', 'name', 'product_type', 'category_id']);
|
||||
}
|
||||
|
||||
// 활성 토글
|
||||
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];
|
||||
}
|
||||
// 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];
|
||||
// }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user