validateProductExists($id); return $this->service->index($id, $request->all()); }, __('message.bom.fetch')); } /** * GET /api/v1/items/{id}/bom/tree * BOM 트리 구조 조회 (계층적) */ public function tree(int $id, Request $request) { return ApiResponse::handle(function () use ($id, $request) { $this->validateProductExists($id); return $this->service->tree($request, $id); }, __('message.bom.fetch')); } /** * POST /api/v1/items/{id}/bom * BOM 라인 추가 (bulk upsert) */ public function store(int $id, Request $request) { return ApiResponse::handle(function () use ($id, $request) { $this->validateProductExists($id); return $this->service->bulkUpsert($id, $request->input('items', [])); }, __('message.bom.created')); } /** * PUT /api/v1/items/{id}/bom/{lineId} * BOM 라인 수정 */ public function update(int $id, int $lineId, Request $request) { return ApiResponse::handle(function () use ($id, $lineId, $request) { $this->validateProductExists($id); return $this->service->update($id, $lineId, $request->all()); }, __('message.bom.updated')); } /** * DELETE /api/v1/items/{id}/bom/{lineId} * BOM 라인 삭제 */ public function destroy(int $id, int $lineId) { return ApiResponse::handle(function () use ($id, $lineId) { $this->validateProductExists($id); $this->service->destroy($id, $lineId); return 'success'; }, __('message.bom.deleted')); } /** * GET /api/v1/items/{id}/bom/summary * BOM 요약 정보 */ public function summary(int $id) { return ApiResponse::handle(function () use ($id) { $this->validateProductExists($id); return $this->service->summary($id); }, __('message.bom.fetch')); } /** * GET /api/v1/items/{id}/bom/validate * BOM 유효성 검사 */ public function validate(int $id) { return ApiResponse::handle(function () use ($id) { $this->validateProductExists($id); return $this->service->validateBom($id); }, __('message.bom.fetch')); } /** * POST /api/v1/items/{id}/bom/replace * BOM 전체 교체 */ public function replace(int $id, Request $request) { return ApiResponse::handle(function () use ($id, $request) { $this->validateProductExists($id); return $this->service->replaceBom($id, $request->all()); }, __('message.bom.created')); } /** * POST /api/v1/items/{id}/bom/reorder * BOM 정렬 변경 */ public function reorder(int $id, Request $request) { return ApiResponse::handle(function () use ($id, $request) { $this->validateProductExists($id); $this->service->reorder($id, $request->input('items', [])); return 'success'; }, __('message.bom.reordered')); } /** * GET /api/v1/items/{id}/bom/categories * 해당 품목의 BOM에서 사용 중인 카테고리 목록 */ public function listCategories(int $id) { return ApiResponse::handle(function () use ($id) { $this->validateProductExists($id); return $this->service->listCategoriesForProduct($id); }, __('message.bom.fetch')); } // ==================== Helper Methods ==================== /** * 품목 ID로 tenant 소유권 검증 * * @throws NotFoundHttpException */ private function validateProductExists(int $id): void { $tenantId = app('tenant_id'); $exists = Product::query() ->where('tenant_id', $tenantId) ->where('id', $id) ->exists(); if (! $exists) { throw new NotFoundHttpException(__('error.not_found')); } } }