diff --git a/app/Http/Controllers/Api/V1/ItemsBomController.php b/app/Http/Controllers/Api/V1/ItemsBomController.php new file mode 100644 index 0000000..9264c8b --- /dev/null +++ b/app/Http/Controllers/Api/V1/ItemsBomController.php @@ -0,0 +1,178 @@ +getProductIdByCode($code); + + return $this->service->index($productId, $request->all()); + }, __('message.bom.fetch')); + } + + /** + * GET /api/v1/items/{code}/bom/tree + * BOM 트리 구조 조회 (계층적) + */ + public function tree(string $code, Request $request) + { + return ApiResponse::handle(function () use ($code, $request) { + $productId = $this->getProductIdByCode($code); + + return $this->service->tree($request, $productId); + }, __('message.bom.fetch')); + } + + /** + * POST /api/v1/items/{code}/bom + * BOM 라인 추가 (bulk upsert) + */ + public function store(string $code, Request $request) + { + return ApiResponse::handle(function () use ($code, $request) { + $productId = $this->getProductIdByCode($code); + + return $this->service->bulkUpsert($productId, $request->input('items', [])); + }, __('message.bom.created')); + } + + /** + * PUT /api/v1/items/{code}/bom/{lineId} + * BOM 라인 수정 + */ + public function update(string $code, int $lineId, Request $request) + { + return ApiResponse::handle(function () use ($code, $lineId, $request) { + $productId = $this->getProductIdByCode($code); + + return $this->service->update($productId, $lineId, $request->all()); + }, __('message.bom.updated')); + } + + /** + * DELETE /api/v1/items/{code}/bom/{lineId} + * BOM 라인 삭제 + */ + public function destroy(string $code, int $lineId) + { + return ApiResponse::handle(function () use ($code, $lineId) { + $productId = $this->getProductIdByCode($code); + + $this->service->destroy($productId, $lineId); + + return 'success'; + }, __('message.bom.deleted')); + } + + /** + * GET /api/v1/items/{code}/bom/summary + * BOM 요약 정보 + */ + public function summary(string $code) + { + return ApiResponse::handle(function () use ($code) { + $productId = $this->getProductIdByCode($code); + + return $this->service->summary($productId); + }, __('message.bom.fetch')); + } + + /** + * GET /api/v1/items/{code}/bom/validate + * BOM 유효성 검사 + */ + public function validate(string $code) + { + return ApiResponse::handle(function () use ($code) { + $productId = $this->getProductIdByCode($code); + + return $this->service->validateBom($productId); + }, __('message.bom.fetch')); + } + + /** + * POST /api/v1/items/{code}/bom/replace + * BOM 전체 교체 + */ + public function replace(string $code, Request $request) + { + return ApiResponse::handle(function () use ($code, $request) { + $productId = $this->getProductIdByCode($code); + + return $this->service->replaceBom($productId, $request->all()); + }, __('message.bom.created')); + } + + /** + * POST /api/v1/items/{code}/bom/reorder + * BOM 정렬 변경 + */ + public function reorder(string $code, Request $request) + { + return ApiResponse::handle(function () use ($code, $request) { + $productId = $this->getProductIdByCode($code); + + $this->service->reorder($productId, $request->input('items', [])); + + return 'success'; + }, __('message.bom.reordered')); + } + + /** + * GET /api/v1/items/{code}/bom/categories + * 해당 품목의 BOM에서 사용 중인 카테고리 목록 + */ + public function listCategories(string $code) + { + return ApiResponse::handle(function () use ($code) { + $productId = $this->getProductIdByCode($code); + + return $this->service->listCategoriesForProduct($productId); + }, __('message.bom.fetch')); + } + + // ==================== Helper Methods ==================== + + /** + * itemCode로 product ID 조회 + * + * @throws NotFoundHttpException + */ + private function getProductIdByCode(string $code): int + { + $tenantId = app('tenant_id'); + + $product = Product::query() + ->where('tenant_id', $tenantId) + ->where('code', $code) + ->first(['id']); + + if (! $product) { + throw new NotFoundHttpException(__('error.not_found')); + } + + return $product->id; + } +} diff --git a/app/Swagger/v1/ItemsBomApi.php b/app/Swagger/v1/ItemsBomApi.php new file mode 100644 index 0000000..1c17a40 --- /dev/null +++ b/app/Swagger/v1/ItemsBomApi.php @@ -0,0 +1,450 @@ + [ 'fetched' => 'BOM 항목을 조회했습니다.', + 'created' => 'BOM 항목이 등록되었습니다.', + 'updated' => 'BOM 항목이 수정되었습니다.', + 'deleted' => 'BOM 항목이 삭제되었습니다.', 'bulk_upsert' => 'BOM 항목이 저장되었습니다.', 'reordered' => 'BOM 정렬이 변경되었습니다.', 'fetch' => 'BOM 항목 조회', diff --git a/routes/api.php b/routes/api.php index 2d8fc7a..a0f8750 100644 --- a/routes/api.php +++ b/routes/api.php @@ -344,6 +344,20 @@ Route::delete('/{code}', [\App\Http\Controllers\Api\V1\ItemsController::class, 'destroy'])->name('v1.items.destroy'); // 품목 삭제 }); + // Items BOM (Code-based BOM API - adapter for frontend) + Route::prefix('items/{code}/bom')->group(function () { + Route::get('', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'index'])->name('v1.items.bom.index'); // BOM 목록 (flat) + Route::get('/tree', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'tree'])->name('v1.items.bom.tree'); // BOM 트리 (계층) + Route::post('', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'store'])->name('v1.items.bom.store'); // BOM 추가 (bulk) + Route::put('/{lineId}', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'update'])->name('v1.items.bom.update'); // BOM 수정 + Route::delete('/{lineId}', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'destroy'])->name('v1.items.bom.destroy'); // BOM 삭제 + Route::get('/summary', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'summary'])->name('v1.items.bom.summary'); // BOM 요약 + Route::get('/validate', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'validate'])->name('v1.items.bom.validate'); // BOM 검증 + Route::post('/replace', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'replace'])->name('v1.items.bom.replace'); // BOM 전체 교체 + Route::post('/reorder', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'reorder'])->name('v1.items.bom.reorder'); // BOM 정렬 + Route::get('/categories', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'listCategories'])->name('v1.items.bom.categories'); // 카테고리 목록 + }); + // BOM (product_components: ref_type=PRODUCT|MATERIAL) Route::prefix('products/{id}/bom')->group(function () { Route::post('/', [ProductBomItemController::class, 'replace'])->name('v1.products.bom.replace');