refactor: CategoryField API를 SAM API Rules에 맞게 리팩토링

- FormRequest 패턴 적용 (CategoryFieldStoreRequest, CategoryFieldUpdateRequest)
- Service에서 Validator::make() 제거
- Controller 메시지 i18n 키로 변경 (__('message.category_field.*'))
- is_required 타입을 'Y'/'N' → boolean으로 통일
- Swagger 스키마 is_required boolean 타입으로 업데이트
- Model scopeRequired() boolean 조건으로 변경
This commit is contained in:
2025-11-14 13:45:54 +09:00
parent e848e12412
commit d4e9f2a6e4
7 changed files with 91 additions and 53 deletions

View File

@@ -4,6 +4,8 @@
use App\Helpers\ApiResponse;
use App\Http\Controllers\Controller;
use App\Http\Requests\CategoryField\CategoryFieldStoreRequest;
use App\Http\Requests\CategoryField\CategoryFieldUpdateRequest;
use App\Services\CategoryFieldService;
use Illuminate\Http\Request;
@@ -16,15 +18,15 @@ public function index(int $id, Request $request)
{
return ApiResponse::handle(function () use ($id, $request) {
return $this->service->index($id, $request->all());
}, '카테고리 필드 목록');
}, __('message.category_field.fetched'));
}
// POST /categories/{id}/fields
public function store(int $id, Request $request)
public function store(int $id, CategoryFieldStoreRequest $request)
{
return ApiResponse::handle(function () use ($id, $request) {
return $this->service->store($id, $request->all());
}, '카테고리 필드 생성');
return $this->service->store($id, $request->validated());
}, __('message.category_field.created'));
}
// GET /categories/fields/{field}
@@ -32,15 +34,15 @@ public function show(int $field)
{
return ApiResponse::handle(function () use ($field) {
return $this->service->show($field);
}, '카테고리 필드 조회');
}, __('message.category_field.fetched'));
}
// PATCH /categories/fields/{field}
public function update(int $field, Request $request)
public function update(int $field, CategoryFieldUpdateRequest $request)
{
return ApiResponse::handle(function () use ($field, $request) {
return $this->service->update($field, $request->all());
}, '카테고리 필드 수정');
return $this->service->update($field, $request->validated());
}, __('message.category_field.updated'));
}
// DELETE /categories/fields/{field}
@@ -50,7 +52,7 @@ public function destroy(int $field)
$this->service->destroy($field);
return 'success';
}, '카테고리 필드 삭제');
}, __('message.category_field.deleted'));
}
// POST /categories/{id}/fields/reorder
@@ -60,7 +62,7 @@ public function reorder(int $id, Request $request)
$this->service->reorder($id, $request->input());
return 'success';
}, '카테고리 필드 정렬 저장');
}, __('message.category_field.reordered'));
}
// PUT /categories/{id}/fields/bulk-upsert
@@ -68,6 +70,6 @@ public function bulkUpsert(int $id, Request $request)
{
return ApiResponse::handle(function () use ($id, $request) {
return $this->service->bulkUpsert($id, $request->input('items', []));
}, '카테고리 필드 일괄 업서트');
}, __('message.category_field.bulk_upsert'));
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Requests\CategoryField;
use Illuminate\Foundation\Http\FormRequest;
class CategoryFieldStoreRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'field_key' => 'required|string|max:30|alpha_dash',
'field_name' => 'required|string|max:100',
'field_type' => 'required|string|max:20',
'is_required' => 'nullable|boolean',
'sort_order' => 'nullable|integer|min:0',
'default_value' => 'nullable|string|max:100',
'options' => 'nullable|json',
'description' => 'nullable|string|max:255',
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Requests\CategoryField;
use Illuminate\Foundation\Http\FormRequest;
class CategoryFieldUpdateRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'field_key' => 'sometimes|string|max:30|alpha_dash',
'field_name' => 'sometimes|string|max:100',
'field_type' => 'sometimes|string|max:20',
'is_required' => 'sometimes|boolean',
'sort_order' => 'sometimes|integer|min:0',
'default_value' => 'nullable|string|max:100',
'options' => 'nullable|json',
'description' => 'nullable|string|max:255',
];
}
}

View File

@@ -34,6 +34,6 @@ public function category()
// 편의 스코프
public function scopeRequired($q)
{
return $q->where('is_required', 1);
return $q->where('is_required', true);
}
}

View File

@@ -5,7 +5,6 @@
use App\Models\Commons\Category;
use App\Models\Commons\CategoryField;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator; // 가정: Eloquent 모델 경로
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class CategoryFieldService extends Service
@@ -32,31 +31,22 @@ public function store(int $categoryId, array $data)
$this->assertCategoryExists($tenantId, $categoryId);
$v = Validator::make($data, [
'field_key' => 'required|string|max:30|alpha_dash',
'field_name' => 'required|string|max:100',
'field_type' => 'required|string|max:20',
'is_required' => 'nullable|in:Y,N',
'sort_order' => 'nullable|integer|min:0',
'default_value' => 'nullable|string|max:100',
'options' => 'nullable|json',
'description' => 'nullable|string|max:255',
]);
$payload = $v->validate();
// FormRequest에서 이미 검증됨
$payload = $data;
// 카테고리 내 field_key 유니크 검증
$exists = CategoryField::query()
->where(compact('tenant_id'))
->where('tenant_id', $tenantId)
->where('category_id', $categoryId)
->where('field_key', $payload['field_key'])
->exists();
if ($exists) {
throw new BadRequestHttpException(__('error.duplicate_key')); // ko/error.php에 매핑
throw new BadRequestHttpException(__('error.duplicate_key'));
}
$payload['tenant_id'] = $tenantId;
$payload['category_id'] = $categoryId;
$payload['is_required'] = $payload['is_required'] ?? 'N';
$payload['is_required'] = $payload['is_required'] ?? false;
$payload['sort_order'] = $payload['sort_order'] ?? 0;
$payload['created_by'] = $userId;
@@ -91,17 +81,8 @@ public function update(int $fieldId, array $data)
throw new BadRequestHttpException(__('error.not_found'));
}
$v = Validator::make($data, [
'field_key' => 'sometimes|string|max:30|alpha_dash',
'field_name' => 'sometimes|string|max:100',
'field_type' => 'sometimes|string|max:20',
'is_required' => 'sometimes|in:Y,N',
'sort_order' => 'sometimes|integer|min:0',
'default_value' => 'nullable|string|max:100',
'options' => 'nullable|json',
'description' => 'nullable|string|max:255',
]);
$payload = $v->validate();
// FormRequest에서 이미 검증됨
$payload = $data;
if (isset($payload['field_key']) && $payload['field_key'] !== $field->field_key) {
$dup = CategoryField::query()
@@ -170,19 +151,11 @@ public function bulkUpsert(int $categoryId, array $items): array
$result = ['created' => 0, 'updated' => 0];
DB::transaction(function () use ($tenantId, $userId, $categoryId, $items, &$result) {
foreach ($items as $it) {
$v = Validator::make($it, [
'id' => 'nullable|integer',
'field_key' => 'sometimes|required_without:id|string|max:30|alpha_dash',
'field_name' => 'required|string|max:100',
'field_type' => 'required|string|max:20',
'is_required' => 'nullable|in:Y,N',
'sort_order' => 'nullable|integer|min:0',
'default_value' => 'nullable|string|max:100',
'options' => 'nullable|json',
'description' => 'nullable|string|max:255',
]);
$payload = $v->validate();
foreach ($items as $payload) {
// 기본적인 검증 (FormRequest가 배열 내부까지는 검증 못함)
if (! isset($payload['field_name'], $payload['field_type'])) {
throw new BadRequestHttpException(__('error.invalid_payload'));
}
if (! empty($payload['id'])) {
$model = CategoryField::query()
@@ -224,7 +197,7 @@ public function bulkUpsert(int $categoryId, array $items): array
$payload['tenant_id'] = $tenantId;
$payload['category_id'] = $categoryId;
$payload['is_required'] = $payload['is_required'] ?? 'N';
$payload['is_required'] = $payload['is_required'] ?? false;
$payload['sort_order'] = $payload['sort_order'] ?? 0;
$payload['created_by'] = $userId;

View File

@@ -26,7 +26,7 @@
* @OA\Property(property="field_key", type="string", example="width"),
* @OA\Property(property="field_name", type="string", example="폭(mm)"),
* @OA\Property(property="field_type", type="string", example="number"),
* @OA\Property(property="is_required", type="string", enum={"Y","N"}, example="N"),
* @OA\Property(property="is_required", type="boolean", example=false),
* @OA\Property(property="sort_order", type="integer", example=1),
* @OA\Property(property="default_value", type="string", nullable=true, example=null),
* @OA\Property(

View File

@@ -65,6 +65,15 @@
'template_applied' => '카테고리 템플릿이 적용되었습니다.',
],
'category_field' => [
'fetched' => '카테고리 필드를 조회했습니다.',
'created' => '카테고리 필드가 생성되었습니다.',
'updated' => '카테고리 필드가 수정되었습니다.',
'deleted' => '카테고리 필드가 삭제되었습니다.',
'reordered' => '카테고리 필드 정렬이 변경되었습니다.',
'bulk_upsert' => '카테고리 필드가 일괄 저장되었습니다.',
],
'design' => [
'template_cloned' => 'BOM 템플릿이 복제되었습니다.',
'template_diff' => 'BOM 템플릿 차이를 계산했습니다.',