diff --git a/app/Http/Controllers/Api/V1/ItemsController.php b/app/Http/Controllers/Api/V1/ItemsController.php index f53748e..3b9ba7c 100644 --- a/app/Http/Controllers/Api/V1/ItemsController.php +++ b/app/Http/Controllers/Api/V1/ItemsController.php @@ -4,6 +4,8 @@ use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; +use App\Http\Requests\Item\ItemStoreRequest; +use App\Http\Requests\Item\ItemUpdateRequest; use App\Services\ItemsService; use Illuminate\Http\Request; @@ -42,4 +44,56 @@ public function show(Request $request, int $id) return $this->service->getItem($itemType, $id, $includePrice, $clientId, $priceDate); }, __('message.fetched')); } -} \ No newline at end of file + + /** + * 품목 상세 조회 (code 기반, BOM 포함 옵션) + * + * GET /api/v1/items/code/{code}?include_bom=true + */ + public function showByCode(Request $request, string $code) + { + return ApiResponse::handle(function () use ($request, $code) { + $includeBom = filter_var($request->input('include_bom', false), FILTER_VALIDATE_BOOLEAN); + + return $this->service->getItemByCode($code, $includeBom); + }, __('message.item.fetched')); + } + + /** + * 품목 생성 + * + * POST /api/v1/items + */ + public function store(ItemStoreRequest $request) + { + return ApiResponse::handle(function () use ($request) { + return $this->service->createItem($request->validated()); + }, __('message.item.created')); + } + + /** + * 품목 수정 + * + * PUT /api/v1/items/{code} + */ + public function update(string $code, ItemUpdateRequest $request) + { + return ApiResponse::handle(function () use ($code, $request) { + return $this->service->updateItem($code, $request->validated()); + }, __('message.item.updated')); + } + + /** + * 품목 삭제 (Soft Delete) + * + * DELETE /api/v1/items/{code} + */ + public function destroy(string $code) + { + return ApiResponse::handle(function () use ($code) { + $this->service->deleteItem($code); + + return 'success'; + }, __('message.item.deleted')); + } +} diff --git a/app/Http/Requests/Item/ItemStoreRequest.php b/app/Http/Requests/Item/ItemStoreRequest.php new file mode 100644 index 0000000..4daeee8 --- /dev/null +++ b/app/Http/Requests/Item/ItemStoreRequest.php @@ -0,0 +1,60 @@ + 'required|string|max:50', + 'name' => 'required|string|max:255', + 'product_type' => 'required|string|in:FG,PT,SM,RM,CS', + 'unit' => 'required|string|max:20', + + // 선택 필드 + 'category_id' => 'nullable|integer|exists:categories,id', + 'description' => 'nullable|string', + + // 플래그 + 'is_sellable' => 'nullable|boolean', + 'is_purchasable' => 'nullable|boolean', + 'is_producible' => 'nullable|boolean', + + // 하이브리드 고정 필드 + 'safety_stock' => 'nullable|integer|min:0', + 'lead_time' => 'nullable|integer|min:0', + 'is_variable_size' => 'nullable|boolean', + 'product_category' => 'nullable|string|max:20', + 'part_type' => 'nullable|string|max:20', + + // 동적 필드 (JSON) + 'attributes' => 'nullable|array', + ]; + } + + public function messages(): array + { + return [ + 'code.required' => '품목코드는 필수입니다.', + 'code.max' => '품목코드는 50자 이내로 입력하세요.', + 'name.required' => '품목명은 필수입니다.', + 'name.max' => '품목명은 255자 이내로 입력하세요.', + 'product_type.required' => '품목 유형은 필수입니다.', + 'product_type.in' => '품목 유형은 FG, PT, SM, RM, CS 중 하나여야 합니다.', + 'unit.required' => '단위는 필수입니다.', + 'unit.max' => '단위는 20자 이내로 입력하세요.', + 'category_id.exists' => '존재하지 않는 카테고리입니다.', + 'safety_stock.min' => '안전재고는 0 이상이어야 합니다.', + 'lead_time.min' => '리드타임은 0 이상이어야 합니다.', + ]; + } +} diff --git a/app/Http/Requests/Item/ItemUpdateRequest.php b/app/Http/Requests/Item/ItemUpdateRequest.php new file mode 100644 index 0000000..3fbbdee --- /dev/null +++ b/app/Http/Requests/Item/ItemUpdateRequest.php @@ -0,0 +1,54 @@ + 'sometimes|string|max:50', + 'name' => 'sometimes|string|max:255', + 'product_type' => 'sometimes|string|in:FG,PT,SM,RM,CS', + 'unit' => 'sometimes|string|max:20', + 'category_id' => 'nullable|integer|exists:categories,id', + 'description' => 'nullable|string', + + // 플래그 + 'is_sellable' => 'sometimes|boolean', + 'is_purchasable' => 'sometimes|boolean', + 'is_producible' => 'sometimes|boolean', + + // 하이브리드 고정 필드 + 'safety_stock' => 'sometimes|integer|min:0', + 'lead_time' => 'sometimes|integer|min:0', + 'is_variable_size' => 'sometimes|boolean', + 'product_category' => 'sometimes|string|max:20', + 'part_type' => 'sometimes|string|max:20', + + // 동적 필드 (JSON) + 'attributes' => 'sometimes|array', + ]; + } + + public function messages(): array + { + return [ + 'code.max' => '품목코드는 50자 이내로 입력하세요.', + 'name.max' => '품목명은 255자 이내로 입력하세요.', + 'product_type.in' => '품목 유형은 FG, PT, SM, RM, CS 중 하나여야 합니다.', + 'unit.max' => '단위는 20자 이내로 입력하세요.', + 'category_id.exists' => '존재하지 않는 카테고리입니다.', + 'safety_stock.min' => '안전재고는 0 이상이어야 합니다.', + 'lead_time.min' => '리드타임은 0 이상이어야 합니다.', + ]; + } +} diff --git a/app/Services/ItemsService.php b/app/Services/ItemsService.php index aa4be79..c4494b2 100644 --- a/app/Services/ItemsService.php +++ b/app/Services/ItemsService.php @@ -5,6 +5,7 @@ use App\Models\Materials\Material; use App\Models\Products\Product; use Illuminate\Support\Facades\DB; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class ItemsService extends Service @@ -220,4 +221,122 @@ private function fetchPrices(string $itemType, int $itemId, ?int $clientId, ?str 'purchase' => $purchasePrice, ]; } -} \ No newline at end of file + + /** + * 품목 생성 (Product 전용) + * + * @param array $data 검증된 데이터 + */ + public function createItem(array $data): Product + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + // 품목 코드 중복 체크 + $exists = Product::query() + ->where('tenant_id', $tenantId) + ->where('code', $data['code']) + ->exists(); + + if ($exists) { + throw new BadRequestHttpException(__('error.duplicate_code')); + } + + $payload = $data; + $payload['tenant_id'] = $tenantId; + $payload['created_by'] = $userId; + $payload['is_sellable'] = $payload['is_sellable'] ?? true; + $payload['is_purchasable'] = $payload['is_purchasable'] ?? false; + $payload['is_producible'] = $payload['is_producible'] ?? false; + + return Product::create($payload); + } + + /** + * 품목 수정 (Product 전용) + * + * @param string $code 품목 코드 + * @param array $data 검증된 데이터 + */ + public function updateItem(string $code, array $data): Product + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + $product = Product::query() + ->where('tenant_id', $tenantId) + ->where('code', $code) + ->first(); + + if (! $product) { + throw new NotFoundHttpException(__('error.not_found')); + } + + // 코드 변경 시 중복 체크 + if (isset($data['code']) && $data['code'] !== $code) { + $exists = Product::query() + ->where('tenant_id', $tenantId) + ->where('code', $data['code']) + ->where('id', '!=', $product->id) + ->exists(); + + if ($exists) { + throw new BadRequestHttpException(__('error.duplicate_code')); + } + } + + $data['updated_by'] = $userId; + $product->update($data); + + return $product->refresh(); + } + + /** + * 품목 삭제 (Product 전용, Soft Delete) + * + * @param string $code 품목 코드 + */ + public function deleteItem(string $code): void + { + $tenantId = $this->tenantId(); + + $product = Product::query() + ->where('tenant_id', $tenantId) + ->where('code', $code) + ->first(); + + if (! $product) { + throw new NotFoundHttpException(__('error.not_found')); + } + + $product->delete(); + } + + /** + * 품목 상세 조회 (code 기반, BOM 포함 옵션) + * + * @param string $code 품목 코드 + * @param bool $includeBom BOM 포함 여부 + */ + public function getItemByCode(string $code, bool $includeBom = false): Product + { + $tenantId = $this->tenantId(); + + $query = Product::query() + ->with('category:id,name') + ->where('tenant_id', $tenantId) + ->where('code', $code); + + if ($includeBom) { + $query->with('componentLines.childProduct:id,code,name,unit'); + } + + $product = $query->first(); + + if (! $product) { + throw new NotFoundHttpException(__('error.not_found')); + } + + return $product; + } +} diff --git a/app/Swagger/v1/ItemsApi.php b/app/Swagger/v1/ItemsApi.php index 8f35146..f5a6eb3 100644 --- a/app/Swagger/v1/ItemsApi.php +++ b/app/Swagger/v1/ItemsApi.php @@ -2,253 +2,203 @@ namespace App\Swagger\v1; -use OpenApi\Annotations as OA; - /** - * @OA\Tag(name="Items", description="통합 품목 조회 (materials + products)") - * - * ========= 공용 스키마 ========= - * - * 통합 품목 스키마 + * @OA\Tag(name="Items", description="품목 관리 (Product CRUD)") * * @OA\Schema( * schema="Item", * type="object", - * description="통합 품목 (PRODUCT 또는 MATERIAL)", + * required={"id","code","name","product_type","unit"}, * * @OA\Property(property="id", type="integer", example=1), - * @OA\Property(property="item_type", type="string", enum={"PRODUCT","MATERIAL"}, example="PRODUCT", description="품목 유형"), - * @OA\Property(property="code", type="string", example="PRD-001", description="제품 코드 또는 자재 코드"), - * @OA\Property(property="name", type="string", example="스크린 모듈 KS001"), - * @OA\Property(property="unit", type="string", nullable=true, example="SET"), - * @OA\Property(property="category_id", type="integer", nullable=true, example=2), - * @OA\Property(property="type_code", type="string", example="FG", description="제품: FG/PT, 자재: SM/RM/CS"), - * @OA\Property(property="created_at", type="string", format="date-time", example="2025-01-10T10:00:00Z") - * ) - * - * 가격 정보 스키마 - * - * @OA\Schema( - * schema="PriceInfo", - * type="object", - * description="가격 정보 (우선순위: 고객그룹가격 → 기본가격)", - * - * @OA\Property(property="price", type="number", format="float", nullable=true, example=15000.5000, description="단가"), - * @OA\Property(property="price_history_id", type="integer", nullable=true, example=123, description="가격 이력 ID"), - * @OA\Property(property="client_group_id", type="integer", nullable=true, example=5, description="고객 그룹 ID"), - * @OA\Property(property="warning", type="string", nullable=true, example="가격 정보를 찾을 수 없습니다.", description="경고 메시지") - * ) - * - * 가격 통합 품목 스키마 - * - * @OA\Schema( - * schema="ItemWithPrice", - * allOf={ - * - * @OA\Schema(ref="#/components/schemas/Item"), - * @OA\Schema( - * type="object", - * - * @OA\Property( - * property="prices", - * type="object", - * description="판매가/매입가 정보", - * - * @OA\Property(property="sale", ref="#/components/schemas/PriceInfo"), - * @OA\Property(property="purchase", ref="#/components/schemas/PriceInfo") - * ) - * ) - * } - * ) - * - * 통합 품목 페이지네이션 - * - * @OA\Schema( - * schema="ItemPagination", - * type="object", - * + * @OA\Property(property="code", type="string", example="P-001"), + * @OA\Property(property="name", type="string", example="스크린 제품 A"), + * @OA\Property(property="product_type", type="string", example="FG", description="FG,PT,SM,RM,CS"), + * @OA\Property(property="unit", type="string", example="EA"), + * @OA\Property(property="category_id", type="integer", nullable=true, example=1), + * @OA\Property(property="description", type="string", nullable=true, example="제품 설명"), + * @OA\Property(property="is_sellable", type="boolean", example=true), + * @OA\Property(property="is_purchasable", type="boolean", example=false), + * @OA\Property(property="is_producible", type="boolean", example=false), + * @OA\Property(property="safety_stock", type="integer", nullable=true, example=10), + * @OA\Property(property="lead_time", type="integer", nullable=true, example=7), + * @OA\Property(property="is_variable_size", type="boolean", example=false), + * @OA\Property(property="product_category", type="string", nullable=true, example="SCREEN"), + * @OA\Property(property="part_type", type="string", nullable=true, example="ASSEMBLY"), * @OA\Property( - * property="data", - * type="array", - * - * @OA\Items(ref="#/components/schemas/Item") + * property="attributes", + * type="object", + * nullable=true, + * description="동적 속성 (JSON)", + * example={"color": "black", "weight": 5.5} * ), + * @OA\Property(property="created_at", type="string", example="2025-11-14 10:00:00"), + * @OA\Property(property="updated_at", type="string", example="2025-11-14 10:10:00") + * ) * - * @OA\Property(property="current_page", type="integer", example=1), - * @OA\Property(property="last_page", type="integer", example=5), - * @OA\Property(property="per_page", type="integer", example=20), - * @OA\Property(property="total", type="integer", example=100), - * @OA\Property(property="from", type="integer", example=1), - * @OA\Property(property="to", type="integer", example=20) + * @OA\Schema( + * schema="ItemCreateRequest", + * type="object", + * required={"code","name","product_type","unit"}, + * + * @OA\Property(property="code", type="string", maxLength=50, example="P-001"), + * @OA\Property(property="name", type="string", maxLength=255, example="스크린 제품 A"), + * @OA\Property(property="product_type", type="string", example="FG", description="FG,PT,SM,RM,CS"), + * @OA\Property(property="unit", type="string", maxLength=20, example="EA"), + * @OA\Property(property="category_id", type="integer", nullable=true, example=1), + * @OA\Property(property="description", type="string", nullable=true, example="제품 설명"), + * @OA\Property(property="is_sellable", type="boolean", nullable=true, example=true), + * @OA\Property(property="is_purchasable", type="boolean", nullable=true, example=false), + * @OA\Property(property="is_producible", type="boolean", nullable=true, example=false), + * @OA\Property(property="safety_stock", type="integer", nullable=true, example=10), + * @OA\Property(property="lead_time", type="integer", nullable=true, example=7), + * @OA\Property(property="is_variable_size", type="boolean", nullable=true, example=false), + * @OA\Property(property="product_category", type="string", nullable=true, example="SCREEN"), + * @OA\Property(property="part_type", type="string", nullable=true, example="ASSEMBLY"), + * @OA\Property( + * property="attributes", + * type="object", + * nullable=true, + * description="동적 속성 (JSON)" + * ) + * ) + * + * @OA\Schema( + * schema="ItemUpdateRequest", + * type="object", + * + * @OA\Property(property="code", type="string", maxLength=50, example="P-001"), + * @OA\Property(property="name", type="string", maxLength=255, example="스크린 제품 A"), + * @OA\Property(property="product_type", type="string", example="FG", description="FG,PT,SM,RM,CS"), + * @OA\Property(property="unit", type="string", maxLength=20, example="EA"), + * @OA\Property(property="category_id", type="integer", nullable=true, example=1), + * @OA\Property(property="description", type="string", nullable=true, example="제품 설명"), + * @OA\Property(property="is_sellable", type="boolean", nullable=true, example=true), + * @OA\Property(property="is_purchasable", type="boolean", nullable=true, example=false), + * @OA\Property(property="is_producible", type="boolean", nullable=true, example=false), + * @OA\Property(property="safety_stock", type="integer", nullable=true, example=10), + * @OA\Property(property="lead_time", type="integer", nullable=true, example=7), + * @OA\Property(property="is_variable_size", type="boolean", nullable=true, example=false), + * @OA\Property(property="product_category", type="string", nullable=true, example="SCREEN"), + * @OA\Property(property="part_type", type="string", nullable=true, example="ASSEMBLY"), + * @OA\Property( + * property="attributes", + * type="object", + * nullable=true, + * description="동적 속성 (JSON)" + * ) * ) */ class ItemsApi { /** - * 통합 품목 목록 조회 - * - * @OA\Get( + * @OA\Post( * path="/api/v1/items", * tags={"Items"}, - * summary="통합 품목 목록 조회 (materials + products UNION)", - * description="제품과 자재를 UNION으로 통합하여 조회합니다. 타입 필터링, 검색, 카테고리 필터 지원.", - * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * summary="품목 생성", + * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, * - * @OA\Parameter( - * name="type", - * in="query", - * description="품목 타입 (쉼표 구분 또는 배열). FG=완제품, PT=부품/서브조립, SM=부자재, RM=원자재, CS=소모품", - * required=false, + * @OA\RequestBody( + * required=true, * - * @OA\Schema( - * type="string", - * example="FG,PT,SM" - * ) - * ), - * - * @OA\Parameter( - * name="search", - * in="query", - * description="검색어 (name, code 필드 LIKE 검색)", - * required=false, - * - * @OA\Schema(type="string", example="스크린") - * ), - * - * @OA\Parameter( - * name="category_id", - * in="query", - * description="카테고리 ID 필터", - * required=false, - * - * @OA\Schema(type="integer", example=2) - * ), - * - * @OA\Parameter( - * name="page", - * in="query", - * description="페이지 번호 (기본: 1)", - * required=false, - * - * @OA\Schema(type="integer", example=1) - * ), - * - * @OA\Parameter( - * name="size", - * in="query", - * description="페이지당 항목 수 (기본: 20)", - * required=false, - * - * @OA\Schema(type="integer", example=20) + * @OA\JsonContent(ref="#/components/schemas/ItemCreateRequest") * ), * * @OA\Response( * response=200, - * description="조회 성공", + * description="성공", * * @OA\JsonContent( - * allOf={ + * type="object", * - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * - * @OA\Property(property="data", ref="#/components/schemas/ItemPagination") - * ) - * } + * @OA\Property(property="success", type="boolean", example=true), + * @OA\Property(property="message", type="string", example="품목이 등록되었습니다."), + * @OA\Property(property="data", ref="#/components/schemas/Item") * ) - * ), - * - * @OA\Response(response=400, description="잘못된 요청", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), - * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), - * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) * ) */ - public function index() {} + public function store() {} /** - * 단일 품목 조회 (가격 정보 포함 가능) - * * @OA\Get( - * path="/api/v1/items/{id}", + * path="/api/v1/items/code/{code}", * tags={"Items"}, - * summary="단일 품목 조회 (가격 정보 포함 가능)", - * description="PRODUCT 또는 MATERIAL 단일 품목 상세 조회. include_price=true로 판매가/매입가 포함 가능.", - * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * summary="품목 코드로 상세 조회", + * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, * - * @OA\Parameter( - * name="id", - * in="path", + * @OA\Parameter(name="code", in="path", required=true, @OA\Schema(type="string"), example="P-001"), + * @OA\Parameter(name="include_bom", in="query", @OA\Schema(type="boolean"), example=false), + * + * @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", ref="#/components/schemas/Item") + * ) + * ) + * ) + */ + public function showByCode() {} + + /** + * @OA\Put( + * path="/api/v1/items/{code}", + * tags={"Items"}, + * summary="품목 수정", + * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * + * @OA\Parameter(name="code", in="path", required=true, @OA\Schema(type="string"), example="P-001"), + * + * @OA\RequestBody( * required=true, - * description="품목 ID", * - * @OA\Schema(type="integer", example=10) - * ), - * - * @OA\Parameter( - * name="item_type", - * in="query", - * required=true, - * description="품목 유형 (PRODUCT 또는 MATERIAL)", - * - * @OA\Schema(type="string", enum={"PRODUCT","MATERIAL"}, example="PRODUCT") - * ), - * - * @OA\Parameter( - * name="include_price", - * in="query", - * required=false, - * description="가격 정보 포함 여부 (기본: false)", - * - * @OA\Schema(type="boolean", example=true) - * ), - * - * @OA\Parameter( - * name="client_id", - * in="query", - * required=false, - * description="고객 ID (가격 조회 시 고객그룹 가격 우선 적용)", - * - * @OA\Schema(type="integer", example=5) - * ), - * - * @OA\Parameter( - * name="price_date", - * in="query", - * required=false, - * description="가격 기준일 (기본: 오늘)", - * - * @OA\Schema(type="string", format="date", example="2025-01-10") + * @OA\JsonContent(ref="#/components/schemas/ItemUpdateRequest") * ), * * @OA\Response( * response=200, - * description="조회 성공", + * description="성공", * * @OA\JsonContent( - * allOf={ + * type="object", * - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * type="object", - * - * @OA\Property( - * property="data", - * oneOf={ - * - * @OA\Schema(ref="#/components/schemas/Item"), - * @OA\Schema(ref="#/components/schemas/ItemWithPrice") - * }, - * description="include_price=false: Item, include_price=true: ItemWithPrice" - * ) - * ) - * } + * @OA\Property(property="success", type="boolean", example=true), + * @OA\Property(property="message", type="string", example="품목이 수정되었습니다."), + * @OA\Property(property="data", ref="#/components/schemas/Item") * ) - * ), - * - * @OA\Response(response=404, description="품목 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), - * @OA\Response(response=400, description="잘못된 요청", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), - * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) * ) */ - public function show() {} -} \ No newline at end of file + public function update() {} + + /** + * @OA\Delete( + * path="/api/v1/items/{code}", + * tags={"Items"}, + * summary="품목 삭제", + * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * + * @OA\Parameter(name="code", in="path", required=true, @OA\Schema(type="string"), example="P-001"), + * + * @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 destroy() {} +} diff --git a/lang/ko/message.php b/lang/ko/message.php index bacd1b1..0eca95c 100644 --- a/lang/ko/message.php +++ b/lang/ko/message.php @@ -30,6 +30,13 @@ 'tenant_switched' => '활성 테넌트가 전환되었습니다.', // 리소스별 세부 (필요 시) + 'item' => [ + 'fetched' => '품목을 조회했습니다.', + 'created' => '품목이 등록되었습니다.', + 'updated' => '품목이 수정되었습니다.', + 'deleted' => '품목이 삭제되었습니다.', + ], + 'product' => [ 'fetched' => '제품을 조회했습니다.', 'category_fetched' => '제품 카테고리를 조회했습니다.', diff --git a/routes/api.php b/routes/api.php index 8176d56..2d8fc7a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -337,7 +337,11 @@ // Items (통합 품목 조회 - materials + products UNION) Route::prefix('items')->group(function () { Route::get('', [\App\Http\Controllers\Api\V1\ItemsController::class, 'index'])->name('v1.items.index'); // 통합 목록 + Route::post('', [\App\Http\Controllers\Api\V1\ItemsController::class, 'store'])->name('v1.items.store'); // 품목 생성 + Route::get('/code/{code}', [\App\Http\Controllers\Api\V1\ItemsController::class, 'showByCode'])->name('v1.items.show_by_code'); // code 기반 조회 Route::get('/{id}', [\App\Http\Controllers\Api\V1\ItemsController::class, 'show'])->name('v1.items.show'); // 단건 (item_type 파라미터 필수) + Route::put('/{code}', [\App\Http\Controllers\Api\V1\ItemsController::class, 'update'])->name('v1.items.update'); // 품목 수정 + Route::delete('/{code}', [\App\Http\Controllers\Api\V1\ItemsController::class, 'destroy'])->name('v1.items.destroy'); // 품목 삭제 }); // BOM (product_components: ref_type=PRODUCT|MATERIAL)