diff --git a/app/Http/Controllers/Api/V1/ItemsController.php b/app/Http/Controllers/Api/V1/ItemsController.php index aea6c78..604d013 100644 --- a/app/Http/Controllers/Api/V1/ItemsController.php +++ b/app/Http/Controllers/Api/V1/ItemsController.php @@ -38,15 +38,15 @@ public function index(Request $request) /** * 단일 품목 조회 (동적 테이블 라우팅) * - * GET /api/v1/items/{id}?item_type=FG&include_price=true&client_id=1&price_date=2025-01-10 + * GET /api/v1/items/{id}?type=FG&include_price=true&client_id=1&price_date=2025-01-10 * - * @param string|null item_type 품목 유형 (선택적 - 없으면 ID만으로 조회) + * @param string|null type 품목 유형 (선택적 - 없으면 ID만으로 조회) */ public function show(Request $request, int $id) { return ApiResponse::handle(function () use ($request, $id) { // item_type 선택적 (없으면 ID만으로 items 테이블에서 조회) - $itemType = $request->input('item_type'); + $itemType = $request->input('type') ?? $request->input('item_type'); $itemType = $itemType ? strtoupper($itemType) : null; $includePrice = filter_var($request->input('include_price', false), FILTER_VALIDATE_BOOLEAN); @@ -64,15 +64,15 @@ public function show(Request $request, int $id) /** * 품목 상세 조회 (동적 테이블 라우팅, code 기반, BOM 포함 옵션) * - * GET /api/v1/items/code/{code}?item_type=FG&include_bom=true + * GET /api/v1/items/code/{code}?type=FG&include_bom=true * - * @param string item_type 품목 유형 (필수 - 동적 테이블 라우팅) + * @param string type 품목 유형 (필수 - 동적 테이블 라우팅) */ public function showByCode(Request $request, string $code) { return ApiResponse::handle(function () use ($request, $code) { // item_type 필수 (동적 테이블 라우팅에 사용) - $itemType = strtoupper($request->input('item_type', '')); + $itemType = strtoupper($request->input('type') ?? $request->input('item_type') ?? ''); $includeBom = filter_var($request->input('include_bom', false), FILTER_VALIDATE_BOOLEAN); return $this->service->showByCode($code, $itemType, $includeBom); @@ -106,15 +106,15 @@ public function update(int $id, ItemUpdateRequest $request) /** * 품목 삭제 (동적 테이블 라우팅, Soft Delete) * - * DELETE /api/v1/items/{id}?item_type=FG + * DELETE /api/v1/items/{id}?type=FG * - * @param string item_type 품목 유형 (필수 - 동적 테이블 라우팅) + * @param string type 품목 유형 (필수 - 동적 테이블 라우팅) */ public function destroy(Request $request, int $id) { return ApiResponse::handle(function () use ($request, $id) { // item_type 필수 (동적 테이블 라우팅에 사용) - $itemType = strtoupper($request->input('item_type', '')); + $itemType = strtoupper($request->input('type') ?? $request->input('item_type') ?? ''); $this->service->destroy($id, $itemType); return 'success'; diff --git a/app/Http/Requests/Item/ItemBatchDeleteRequest.php b/app/Http/Requests/Item/ItemBatchDeleteRequest.php index 8f64d28..5e04d7f 100644 --- a/app/Http/Requests/Item/ItemBatchDeleteRequest.php +++ b/app/Http/Requests/Item/ItemBatchDeleteRequest.php @@ -11,6 +11,14 @@ public function authorize(): bool return true; } + protected function prepareForValidation(): void + { + // type 파라미터를 item_type으로 매핑 (일관성) + if ($this->has('type') && ! $this->has('item_type')) { + $this->merge(['item_type' => $this->input('type')]); + } + } + public function rules(): array { return [ diff --git a/app/Services/ItemService.php b/app/Services/ItemService.php index edd0548..dea89cb 100644 --- a/app/Services/ItemService.php +++ b/app/Services/ItemService.php @@ -118,6 +118,59 @@ private function newQueryForTypes(array $itemTypes) ->whereIn('item_type', array_map('strtoupper', $itemTypes)); } + /** + * 콤마 구분 item_type 문자열을 배열로 파싱 + * + * @param string|null $itemType 콤마 구분 item_type (예: "FG,PT") + * @return array 정규화된 item_type 배열 + */ + private function parseItemTypes(?string $itemType): array + { + if (! $itemType) { + return []; + } + + return array_filter(array_map('trim', explode(',', strtoupper($itemType)))); + } + + /** + * 여러 item_type이 같은 group_id에 속하는지 검증 + * + * @param array $itemTypes item_type 배열 + * @return int 공통 group_id + * + * @throws BadRequestHttpException 다른 group_id에 속하면 예외 + */ + private function validateItemTypesInSameGroup(array $itemTypes): int + { + if (empty($itemTypes)) { + throw new BadRequestHttpException(__('error.item_type_required')); + } + + // item_type 코드들의 parent_id (group) 조회 + $groupCodes = \DB::table('common_codes as t') + ->join('common_codes as g', 't.parent_id', '=', 'g.id') + ->where('t.code_group', 'item_type') + ->whereIn('t.code', $itemTypes) + ->where('t.is_active', true) + ->where('g.code_group', 'group') + ->distinct() + ->pluck('g.code') + ->toArray(); + + // 존재하지 않는 item_type이 있는 경우 + if (count($groupCodes) === 0) { + throw new BadRequestHttpException(__('error.invalid_item_type')); + } + + // 여러 그룹에 속한 경우 + if (count($groupCodes) > 1) { + throw new BadRequestHttpException(__('error.item_types_must_be_same_group')); + } + + return (int) $groupCodes[0]; + } + /** * items 테이블의 고정 컬럼 목록 조회 (SystemFields + ItemField 기반) */ @@ -316,9 +369,19 @@ public function index(array $params): LengthAwarePaginator $query = $this->newQueryForTypes($itemTypes) ->with(['category:id,name', 'details', 'files']); } else { - // 단일 item_type 조회 - $query = $this->newQuery($itemType) - ->with(['category:id,name', 'details', 'files']); + // item_type 조회 (단일 또는 콤마 구분 멀티) + $itemTypes = $this->parseItemTypes($itemType); + + if (count($itemTypes) === 1) { + // 단일 item_type + $query = $this->newQuery($itemTypes[0]) + ->with(['category:id,name', 'details', 'files']); + } else { + // 멀티 item_type - 같은 그룹인지 검증 + $this->validateItemTypesInSameGroup($itemTypes); + $query = $this->newQueryForTypes($itemTypes) + ->with(['category:id,name', 'details', 'files']); + } } // 검색어 @@ -651,7 +714,7 @@ public function destroy(int $id, string $itemType): void /** * 간편 검색 (동적 테이블 라우팅, 모달/드롭다운) * - * @param array $params 검색 파라미터 (item_type 필수) + * @param array $params 검색 파라미터 (item_type 필수, 콤마 구분 멀티 지원) */ public function search(array $params) { @@ -664,8 +727,17 @@ public function search(array $params) throw new BadRequestHttpException(__('error.item_type_required')); } - // 동적 테이블 라우팅 - $query = $this->newQuery($itemType); + // item_type 파싱 (단일 또는 콤마 구분 멀티) + $itemTypes = $this->parseItemTypes($itemType); + + if (count($itemTypes) === 1) { + // 단일 item_type - 동적 테이블 라우팅 + $query = $this->newQuery($itemTypes[0]); + } else { + // 멀티 item_type - 같은 그룹인지 검증 + $this->validateItemTypesInSameGroup($itemTypes); + $query = $this->newQueryForTypes($itemTypes); + } if ($q !== '') { $query->where(function ($w) use ($q) { diff --git a/lang/en/error.php b/lang/en/error.php index b883e46..a06801e 100644 --- a/lang/en/error.php +++ b/lang/en/error.php @@ -76,6 +76,14 @@ 'has_clients' => 'Cannot delete the client group because it has associated clients.', 'code_exists_in_deleted' => 'The same code exists in deleted data. Please permanently delete that code first or use a different code.', + // Item type validation + 'item_type_required' => 'Item type (item_type) is required.', + 'item_type_or_group_required' => 'Item type (item_type) or group ID (group_id) is required.', + 'invalid_item_type' => 'Invalid item type.', + 'invalid_group_id' => 'Invalid group ID or no item types exist in the specified group.', + 'item_types_must_be_same_group' => 'When selecting multiple item types, all types must belong to the same group.', + 'invalid_source_table' => 'Source table is not configured for this item type.', + // Item management related 'item' => [ 'not_found' => 'Item not found.', diff --git a/lang/ko/error.php b/lang/ko/error.php index f9013a5..687c512 100644 --- a/lang/ko/error.php +++ b/lang/ko/error.php @@ -117,6 +117,7 @@ 'item_type_or_group_required' => '품목 유형(item_type) 또는 그룹 ID(group_id)는 필수입니다.', 'invalid_item_type' => '유효하지 않은 품목 유형입니다.', 'invalid_group_id' => '유효하지 않은 그룹 ID이거나 해당 그룹에 품목 유형이 없습니다.', + 'item_types_must_be_same_group' => '여러 품목 유형을 선택할 때는 같은 그룹에 속한 유형만 선택할 수 있습니다.', 'invalid_source_table' => '품목 유형에 대한 소스 테이블이 설정되지 않았습니다.', // 품목 관리 관련