feat: [items] 아이템 API 기능 개선

- ItemsController, ItemsBomController, ItemsFileController 수정
- ItemBatchDeleteRequest 추가
- ItemsService 개선
- ItemsApi Swagger 문서 업데이트
This commit is contained in:
2025-11-30 21:05:44 +09:00
parent d27e47108d
commit f09fa3791c
6 changed files with 208 additions and 97 deletions

View File

@@ -10,169 +10,167 @@
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Items BOM Controller (Code-based Adapter)
* Items BOM Controller (ID-based)
*
* 프론트엔드 요구사항에 맞춰 itemCode 기반으로 BOM을 관리하는 Adapter
* 내부적으로 code → id 변환 후 기존 ProductBomService 재사용
* ID 기반으로 BOM을 관리하는 컨트롤러
* 내부적으로 기존 ProductBomService 재사용
*/
class ItemsBomController extends Controller
{
public function __construct(private ProductBomService $service) {}
/**
* GET /api/v1/items/{code}/bom
* GET /api/v1/items/{id}/bom
* BOM 라인 목록 조회 (flat list)
*/
public function index(string $code, Request $request)
public function index(int $id, Request $request)
{
return ApiResponse::handle(function () use ($code, $request) {
$productId = $this->getProductIdByCode($code);
return ApiResponse::handle(function () use ($id, $request) {
$this->validateProductExists($id);
return $this->service->index($productId, $request->all());
return $this->service->index($id, $request->all());
}, __('message.bom.fetch'));
}
/**
* GET /api/v1/items/{code}/bom/tree
* GET /api/v1/items/{id}/bom/tree
* BOM 트리 구조 조회 (계층적)
*/
public function tree(string $code, Request $request)
public function tree(int $id, Request $request)
{
return ApiResponse::handle(function () use ($code, $request) {
$productId = $this->getProductIdByCode($code);
return ApiResponse::handle(function () use ($id, $request) {
$this->validateProductExists($id);
return $this->service->tree($request, $productId);
return $this->service->tree($request, $id);
}, __('message.bom.fetch'));
}
/**
* POST /api/v1/items/{code}/bom
* POST /api/v1/items/{id}/bom
* BOM 라인 추가 (bulk upsert)
*/
public function store(string $code, Request $request)
public function store(int $id, Request $request)
{
return ApiResponse::handle(function () use ($code, $request) {
$productId = $this->getProductIdByCode($code);
return ApiResponse::handle(function () use ($id, $request) {
$this->validateProductExists($id);
return $this->service->bulkUpsert($productId, $request->input('items', []));
return $this->service->bulkUpsert($id, $request->input('items', []));
}, __('message.bom.created'));
}
/**
* PUT /api/v1/items/{code}/bom/{lineId}
* PUT /api/v1/items/{id}/bom/{lineId}
* BOM 라인 수정
*/
public function update(string $code, int $lineId, Request $request)
public function update(int $id, int $lineId, Request $request)
{
return ApiResponse::handle(function () use ($code, $lineId, $request) {
$productId = $this->getProductIdByCode($code);
return ApiResponse::handle(function () use ($id, $lineId, $request) {
$this->validateProductExists($id);
return $this->service->update($productId, $lineId, $request->all());
return $this->service->update($id, $lineId, $request->all());
}, __('message.bom.updated'));
}
/**
* DELETE /api/v1/items/{code}/bom/{lineId}
* DELETE /api/v1/items/{id}/bom/{lineId}
* BOM 라인 삭제
*/
public function destroy(string $code, int $lineId)
public function destroy(int $id, int $lineId)
{
return ApiResponse::handle(function () use ($code, $lineId) {
$productId = $this->getProductIdByCode($code);
return ApiResponse::handle(function () use ($id, $lineId) {
$this->validateProductExists($id);
$this->service->destroy($productId, $lineId);
$this->service->destroy($id, $lineId);
return 'success';
}, __('message.bom.deleted'));
}
/**
* GET /api/v1/items/{code}/bom/summary
* GET /api/v1/items/{id}/bom/summary
* BOM 요약 정보
*/
public function summary(string $code)
public function summary(int $id)
{
return ApiResponse::handle(function () use ($code) {
$productId = $this->getProductIdByCode($code);
return ApiResponse::handle(function () use ($id) {
$this->validateProductExists($id);
return $this->service->summary($productId);
return $this->service->summary($id);
}, __('message.bom.fetch'));
}
/**
* GET /api/v1/items/{code}/bom/validate
* GET /api/v1/items/{id}/bom/validate
* BOM 유효성 검사
*/
public function validate(string $code)
public function validate(int $id)
{
return ApiResponse::handle(function () use ($code) {
$productId = $this->getProductIdByCode($code);
return ApiResponse::handle(function () use ($id) {
$this->validateProductExists($id);
return $this->service->validateBom($productId);
return $this->service->validateBom($id);
}, __('message.bom.fetch'));
}
/**
* POST /api/v1/items/{code}/bom/replace
* POST /api/v1/items/{id}/bom/replace
* BOM 전체 교체
*/
public function replace(string $code, Request $request)
public function replace(int $id, Request $request)
{
return ApiResponse::handle(function () use ($code, $request) {
$productId = $this->getProductIdByCode($code);
return ApiResponse::handle(function () use ($id, $request) {
$this->validateProductExists($id);
return $this->service->replaceBom($productId, $request->all());
return $this->service->replaceBom($id, $request->all());
}, __('message.bom.created'));
}
/**
* POST /api/v1/items/{code}/bom/reorder
* POST /api/v1/items/{id}/bom/reorder
* BOM 정렬 변경
*/
public function reorder(string $code, Request $request)
public function reorder(int $id, Request $request)
{
return ApiResponse::handle(function () use ($code, $request) {
$productId = $this->getProductIdByCode($code);
return ApiResponse::handle(function () use ($id, $request) {
$this->validateProductExists($id);
$this->service->reorder($productId, $request->input('items', []));
$this->service->reorder($id, $request->input('items', []));
return 'success';
}, __('message.bom.reordered'));
}
/**
* GET /api/v1/items/{code}/bom/categories
* GET /api/v1/items/{id}/bom/categories
* 해당 품목의 BOM에서 사용 중인 카테고리 목록
*/
public function listCategories(string $code)
public function listCategories(int $id)
{
return ApiResponse::handle(function () use ($code) {
$productId = $this->getProductIdByCode($code);
return ApiResponse::handle(function () use ($id) {
$this->validateProductExists($id);
return $this->service->listCategoriesForProduct($productId);
return $this->service->listCategoriesForProduct($id);
}, __('message.bom.fetch'));
}
// ==================== Helper Methods ====================
/**
* itemCode로 product ID 조회
* 품목 ID로 tenant 소유권 검증
*
* @throws NotFoundHttpException
*/
private function getProductIdByCode(string $code): int
private function validateProductExists(int $id): void
{
$tenantId = app('tenant_id');
$product = Product::query()
$exists = Product::query()
->where('tenant_id', $tenantId)
->where('code', $code)
->first(['id']);
->where('id', $id)
->exists();
if (! $product) {
if (! $exists) {
throw new NotFoundHttpException(__('error.not_found'));
}
return $product->id;
}
}

View File

@@ -4,6 +4,7 @@
use App\Helpers\ApiResponse;
use App\Http\Controllers\Controller;
use App\Http\Requests\Item\ItemBatchDeleteRequest;
use App\Http\Requests\Item\ItemStoreRequest;
use App\Http\Requests\Item\ItemUpdateRequest;
use App\Services\ItemsService;
@@ -74,26 +75,40 @@ public function store(ItemStoreRequest $request)
/**
* 품목 수정
*
* PUT /api/v1/items/{code}
* PUT /api/v1/items/{id}
*/
public function update(string $code, ItemUpdateRequest $request)
public function update(int $id, ItemUpdateRequest $request)
{
return ApiResponse::handle(function () use ($code, $request) {
return $this->service->updateItem($code, $request->validated());
return ApiResponse::handle(function () use ($id, $request) {
return $this->service->updateItem($id, $request->validated());
}, __('message.item.updated'));
}
/**
* 품목 삭제 (Soft Delete)
*
* DELETE /api/v1/items/{code}
* DELETE /api/v1/items/{id}
*/
public function destroy(string $code)
public function destroy(int $id)
{
return ApiResponse::handle(function () use ($code) {
$this->service->deleteItem($code);
return ApiResponse::handle(function () use ($id) {
$this->service->deleteItem($id);
return 'success';
}, __('message.item.deleted'));
}
/**
* 품목 일괄 삭제 (Soft Delete)
*
* DELETE /api/v1/items/batch
*/
public function batchDestroy(ItemBatchDeleteRequest $request)
{
return ApiResponse::handle(function () use ($request) {
$this->service->batchDeleteItems($request->validated()['ids']);
return 'success';
}, __('message.item.batch_deleted'));
}
}

View File

@@ -14,7 +14,7 @@
/**
* 품목 파일 관리 컨트롤러
*
* Code-based 파일 업로드/삭제 API
* ID-based 파일 업로드/삭제 API
* - 절곡도 (bending_diagram)
* - 시방서 (specification)
* - 인정서 (certification)
@@ -24,18 +24,18 @@ class ItemsFileController extends Controller
/**
* 파일 업로드
*
* POST /api/v1/items/{code}/files
* POST /api/v1/items/{id}/files
*/
public function upload(string $code, ItemsFileUploadRequest $request)
public function upload(int $id, ItemsFileUploadRequest $request)
{
return ApiResponse::handle(function () use ($code, $request) {
$product = $this->getProductByCode($code);
return ApiResponse::handle(function () use ($id, $request) {
$product = $this->getProductById($id);
$validated = $request->validated();
$fileType = $request->route('type') ?? $validated['type'];
$file = $validated['file'];
// 파일 저장 경로: items/{code}/{type}/{filename}
$directory = sprintf('items/%s/%s', $code, $fileType);
// 파일 저장 경로: items/{id}/{type}/{filename}
$directory = sprintf('items/%d/%s', $id, $fileType);
$filePath = Storage::disk('public')->putFile($directory, $file);
$fileUrl = Storage::disk('public')->url($filePath);
$originalName = $file->getClientOriginalName();
@@ -57,12 +57,12 @@ public function upload(string $code, ItemsFileUploadRequest $request)
/**
* 파일 삭제
*
* DELETE /api/v1/items/{code}/files/{type}
* DELETE /api/v1/items/{id}/files/{type}
*/
public function delete(string $code, string $type, Request $request)
public function delete(int $id, string $type, Request $request)
{
return ApiResponse::handle(function () use ($code, $type) {
$product = $this->getProductByCode($code);
return ApiResponse::handle(function () use ($id, $type) {
$product = $this->getProductById($id);
// 파일 타입 검증
if (! in_array($type, ['bending_diagram', 'specification', 'certification'])) {
@@ -90,15 +90,14 @@ public function delete(string $code, string $type, Request $request)
}
/**
* 코드로 Product 조회
* ID로 Product 조회
*/
private function getProductByCode(string $code): Product
private function getProductById(int $id): Product
{
$tenantId = app('tenant_id');
$product = Product::query()
->where('tenant_id', $tenantId)
->where('code', $code)
->first();
->find($id);
if (! $product) {
throw new NotFoundHttpException(__('error.not_found'));