From e848e124124c000895f594f4b3a43f560fb319a1 Mon Sep 17 00:00:00 2001 From: hskwon Date: Fri, 14 Nov 2025 12:42:08 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Product=20API=20=ED=95=98=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=A6=AC=EB=93=9C=20=EA=B5=AC=EC=A1=B0=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=20(Phase=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [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 기능도 필요시 복원 가능 (주석으로 보존) --- .../Controllers/Api/V1/ProductController.php | 14 +-- .../Requests/Product/ProductStoreRequest.php | 23 +++-- .../Requests/Product/ProductUpdateRequest.php | 23 +++-- app/Services/ProductService.php | 88 +++++++------------ 4 files changed, 78 insertions(+), 70 deletions(-) diff --git a/app/Http/Controllers/Api/V1/ProductController.php b/app/Http/Controllers/Api/V1/ProductController.php index 04cecf8..65f1765 100644 --- a/app/Http/Controllers/Api/V1/ProductController.php +++ b/app/Http/Controllers/Api/V1/ProductController.php @@ -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')); + // } } diff --git a/app/Http/Requests/Product/ProductStoreRequest.php b/app/Http/Requests/Product/ProductStoreRequest.php index c47a4ce..91ece77 100644 --- a/app/Http/Requests/Product/ProductStoreRequest.php +++ b/app/Http/Requests/Product/ProductStoreRequest.php @@ -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', ]; } } diff --git a/app/Http/Requests/Product/ProductUpdateRequest.php b/app/Http/Requests/Product/ProductUpdateRequest.php index 7a581d6..6379e53 100644 --- a/app/Http/Requests/Product/ProductUpdateRequest.php +++ b/app/Http/Requests/Product/ProductUpdateRequest.php @@ -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', ]; } } diff --git a/app/Services/ProductService.php b/app/Services/ProductService.php index dafef05..d1bb912 100644 --- a/app/Services/ProductService.php +++ b/app/Services/ProductService.php @@ -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]; + // } }