feat: [items] 아이템 API 기능 개선
- ItemsController, ItemsBomController, ItemsFileController 수정 - ItemBatchDeleteRequest 추가 - ItemsService 개선 - ItemsApi Swagger 문서 업데이트
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
|
||||
33
app/Http/Requests/Item/ItemBatchDeleteRequest.php
Normal file
33
app/Http/Requests/Item/ItemBatchDeleteRequest.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Item;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ItemBatchDeleteRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'ids' => 'required|array|min:1',
|
||||
'ids.*' => 'required|integer|min:1',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'ids.required' => '삭제할 품목 ID 목록은 필수입니다.',
|
||||
'ids.array' => '품목 ID 목록은 배열이어야 합니다.',
|
||||
'ids.min' => '삭제할 품목을 하나 이상 선택하세요.',
|
||||
'ids.*.required' => '품목 ID는 필수입니다.',
|
||||
'ids.*.integer' => '품목 ID는 정수여야 합니다.',
|
||||
'ids.*.min' => '품목 ID는 1 이상이어야 합니다.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -255,25 +255,24 @@ public function createItem(array $data): Product
|
||||
/**
|
||||
* 품목 수정 (Product 전용)
|
||||
*
|
||||
* @param string $code 품목 코드
|
||||
* @param int $id 품목 ID
|
||||
* @param array $data 검증된 데이터
|
||||
*/
|
||||
public function updateItem(string $code, array $data): Product
|
||||
public function updateItem(int $id, array $data): Product
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$product = Product::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $code)
|
||||
->first();
|
||||
->find($id);
|
||||
|
||||
if (! $product) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
// 코드 변경 시 중복 체크
|
||||
if (isset($data['code']) && $data['code'] !== $code) {
|
||||
if (isset($data['code']) && $data['code'] !== $product->code) {
|
||||
$exists = Product::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $data['code'])
|
||||
@@ -294,16 +293,15 @@ public function updateItem(string $code, array $data): Product
|
||||
/**
|
||||
* 품목 삭제 (Product 전용, Soft Delete)
|
||||
*
|
||||
* @param string $code 품목 코드
|
||||
* @param int $id 품목 ID
|
||||
*/
|
||||
public function deleteItem(string $code): void
|
||||
public function deleteItem(int $id): void
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$product = Product::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $code)
|
||||
->first();
|
||||
->find($id);
|
||||
|
||||
if (! $product) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
@@ -312,6 +310,29 @@ public function deleteItem(string $code): void
|
||||
$product->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 일괄 삭제 (Product 전용, Soft Delete)
|
||||
*
|
||||
* @param array $ids 품목 ID 배열
|
||||
*/
|
||||
public function batchDeleteItems(array $ids): void
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$products = Product::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('id', $ids)
|
||||
->get();
|
||||
|
||||
if ($products->isEmpty()) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
foreach ($products as $product) {
|
||||
$product->delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 상세 조회 (code 기반, BOM 포함 옵션)
|
||||
*
|
||||
|
||||
@@ -88,6 +88,21 @@
|
||||
* description="동적 속성 (JSON)"
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ItemBatchDeleteRequest",
|
||||
* type="object",
|
||||
* required={"ids"},
|
||||
*
|
||||
* @OA\Property(
|
||||
* property="ids",
|
||||
* type="array",
|
||||
* description="삭제할 품목 ID 목록",
|
||||
*
|
||||
* @OA\Items(type="integer"),
|
||||
* example={1, 2, 3}
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
class ItemsApi
|
||||
{
|
||||
@@ -183,12 +198,12 @@ public function showByCode() {}
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/v1/items/{code}",
|
||||
* path="/api/v1/items/{id}",
|
||||
* tags={"Items"},
|
||||
* summary="품목 수정",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="code", in="path", required=true, @OA\Schema(type="string"), example="P-001"),
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer"), example=1, description="품목 ID"),
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
@@ -214,12 +229,12 @@ public function update() {}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/v1/items/{code}",
|
||||
* path="/api/v1/items/{id}",
|
||||
* tags={"Items"},
|
||||
* summary="품목 삭제",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="code", in="path", required=true, @OA\Schema(type="string"), example="P-001"),
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer"), example=1, description="품목 ID"),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
@@ -236,4 +251,34 @@ public function update() {}
|
||||
* )
|
||||
*/
|
||||
public function destroy() {}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/v1/items/batch",
|
||||
* tags={"Items"},
|
||||
* summary="품목 일괄 삭제",
|
||||
* description="여러 품목을 한 번에 삭제합니다.",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/ItemBatchDeleteRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string", example="품목이 일괄 삭제되었습니다."),
|
||||
* @OA\Property(property="data", type="string", example="success")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function batchDestroy() {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user