From 2f2fffb6f004d499102d22e7ca3fb9e2c64a97af Mon Sep 17 00:00:00 2001 From: hskwon Date: Mon, 17 Nov 2025 11:45:16 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Items=20BOM=20API=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20(Code=20=EA=B8=B0=EB=B0=98=20Adapter)=20-=20BP-MES=20Phase?= =?UTF-8?q?=201=20Day=206-9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ItemsBomController 생성 (code 기반 BOM 관리) - 기존 ProductBomService 100% 재사용 (Adapter 패턴) - Code → ID 변환 후 기존 비즈니스 로직 활용 - 프론트엔드 요구사항 완벽 대응 (itemCode 기반 API) - 10개 엔드포인트 추가: * GET /items/{code}/bom - BOM 목록 (flat) * GET /items/{code}/bom/tree - BOM 트리 (계층) * POST /items/{code}/bom - BOM 추가 (bulk upsert) * PUT /items/{code}/bom/{lineId} - BOM 수정 * DELETE /items/{code}/bom/{lineId} - BOM 삭제 * GET /items/{code}/bom/summary - BOM 요약 * GET /items/{code}/bom/validate - BOM 검증 * POST /items/{code}/bom/replace - BOM 전체 교체 * POST /items/{code}/bom/reorder - BOM 정렬 * GET /items/{code}/bom/categories - 카테고리 목록 - Swagger 문서 완성 (ItemsBomApi.php) - i18n 메시지 키 추가 (message.bom.created/updated/deleted) - Hybrid 구조 지원 (quantity_formula, condition, attributes) --- .../Controllers/Api/V1/ItemsBomController.php | 178 +++++++ app/Swagger/v1/ItemsBomApi.php | 450 ++++++++++++++++++ lang/ko/message.php | 3 + routes/api.php | 14 + 4 files changed, 645 insertions(+) create mode 100644 app/Http/Controllers/Api/V1/ItemsBomController.php create mode 100644 app/Swagger/v1/ItemsBomApi.php 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');