feat: ItemMaster Phase 3 API 구현 (부가 기능 8개 엔드포인트)

- Controller 2개, Service 2개, FormRequest 3개 생성
- Routes 등록 (커스텀 탭, 단위 옵션)
- Service-First, Multi-tenant, Soft Delete
- 라우트 테스트 및 Pint 검사 통과
- ItemMaster API 전체 32개 엔드포인트 구현 완료

9 files changed, 467 insertions(+)
This commit is contained in:
2025-11-20 17:16:03 +09:00
parent ddfcaabfb2
commit a85cf0a144
9 changed files with 492 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers\Api\V1\ItemMaster;
use App\Http\Controllers\Controller;
use App\Http\Requests\ItemMaster\CustomTabStoreRequest;
use App\Http\Requests\ItemMaster\CustomTabUpdateRequest;
use App\Http\Requests\ItemMaster\ReorderRequest;
use App\Http\Responses\ApiResponse;
use App\Services\ItemMaster\CustomTabService;
class CustomTabController extends Controller
{
public function __construct(
protected CustomTabService $service,
) {}
/**
* 커스텀 탭 목록
*/
public function index()
{
return ApiResponse::handle(function () {
return $this->service->index();
}, __('message.fetched'));
}
/**
* 커스텀 탭 생성
*/
public function store(CustomTabStoreRequest $request)
{
return ApiResponse::handle(function () use ($request) {
return $this->service->store($request->validated());
}, __('message.created'));
}
/**
* 커스텀 탭 수정
*/
public function update(int $id, CustomTabUpdateRequest $request)
{
return ApiResponse::handle(function () use ($id, $request) {
return $this->service->update($id, $request->validated());
}, __('message.updated'));
}
/**
* 커스텀 탭 삭제
*/
public function destroy(int $id)
{
return ApiResponse::handle(function () use ($id) {
$this->service->destroy($id);
return 'success';
}, __('message.deleted'));
}
/**
* 커스텀 탭 순서 변경
*/
public function reorder(ReorderRequest $request)
{
return ApiResponse::handle(function () use ($request) {
$this->service->reorder($request->validated()['items']);
return 'success';
}, __('message.reordered'));
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Http\Controllers\Api\V1\ItemMaster;
use App\Http\Controllers\Controller;
use App\Http\Requests\ItemMaster\UnitOptionStoreRequest;
use App\Http\Responses\ApiResponse;
use App\Services\ItemMaster\UnitOptionService;
class UnitOptionController extends Controller
{
public function __construct(
protected UnitOptionService $service,
) {}
/**
* 단위 옵션 목록
*/
public function index()
{
return ApiResponse::handle(function () {
return $this->service->index();
}, __('message.fetched'));
}
/**
* 단위 옵션 생성
*/
public function store(UnitOptionStoreRequest $request)
{
return ApiResponse::handle(function () use ($request) {
return $this->service->store($request->validated());
}, __('message.created'));
}
/**
* 단위 옵션 삭제
*/
public function destroy(int $id)
{
return ApiResponse::handle(function () use ($id) {
$this->service->destroy($id);
return 'success';
}, __('message.deleted'));
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests\ItemMaster;
use Illuminate\Foundation\Http\FormRequest;
class CustomTabStoreRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'label' => 'required|string|max:255',
'icon' => 'nullable|string|max:100',
'is_default' => 'nullable|boolean',
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests\ItemMaster;
use Illuminate\Foundation\Http\FormRequest;
class CustomTabUpdateRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'label' => 'sometimes|string|max:255',
'icon' => 'sometimes|nullable|string|max:100',
'is_default' => 'sometimes|nullable|boolean',
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\ItemMaster;
use Illuminate\Foundation\Http\FormRequest;
class UnitOptionStoreRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'label' => 'required|string|max:100',
'value' => 'required|string|max:50',
];
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace App\Services\ItemMaster;
use App\Models\ItemMaster\CustomTab;
use App\Services\Service;
use Illuminate\Database\Eloquent\Collection;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class CustomTabService extends Service
{
/**
* 커스텀 탭 목록
*/
public function index(): Collection
{
$tenantId = $this->tenantId();
return CustomTab::with('columnSetting')
->where('tenant_id', $tenantId)
->orderBy('order_no')
->get();
}
/**
* 커스텀 탭 생성
*/
public function store(array $data): CustomTab
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$maxOrder = CustomTab::where('tenant_id', $tenantId)
->max('order_no');
$tab = CustomTab::create([
'tenant_id' => $tenantId,
'label' => $data['label'],
'icon' => $data['icon'] ?? null,
'is_default' => $data['is_default'] ?? false,
'order_no' => ($maxOrder ?? -1) + 1,
'created_by' => $userId,
]);
return $tab->load('columnSetting');
}
/**
* 커스텀 탭 수정
*/
public function update(int $id, array $data): CustomTab
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$tab = CustomTab::where('tenant_id', $tenantId)
->where('id', $id)
->first();
if (! $tab) {
throw new NotFoundHttpException(__('error.not_found'));
}
$tab->update([
'label' => $data['label'] ?? $tab->label,
'icon' => $data['icon'] ?? $tab->icon,
'is_default' => $data['is_default'] ?? $tab->is_default,
'updated_by' => $userId,
]);
return $tab->load('columnSetting');
}
/**
* 커스텀 탭 삭제
*/
public function destroy(int $id): void
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$tab = CustomTab::where('tenant_id', $tenantId)
->where('id', $id)
->first();
if (! $tab) {
throw new NotFoundHttpException(__('error.not_found'));
}
$tab->update(['deleted_by' => $userId]);
$tab->delete();
}
/**
* 커스텀 탭 순서 변경
*/
public function reorder(array $items): void
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
foreach ($items as $item) {
CustomTab::where('tenant_id', $tenantId)
->where('id', $item['id'])
->update([
'order_no' => $item['order_no'],
'updated_by' => $userId,
]);
}
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Services\ItemMaster;
use App\Models\ItemMaster\UnitOption;
use App\Services\Service;
use Illuminate\Database\Eloquent\Collection;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class UnitOptionService extends Service
{
/**
* 단위 옵션 목록
*/
public function index(): Collection
{
$tenantId = $this->tenantId();
return UnitOption::where('tenant_id', $tenantId)
->orderBy('label')
->get();
}
/**
* 단위 옵션 생성
*/
public function store(array $data): UnitOption
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$unit = UnitOption::create([
'tenant_id' => $tenantId,
'label' => $data['label'],
'value' => $data['value'],
'created_by' => $userId,
]);
return $unit;
}
/**
* 단위 옵션 삭제
*/
public function destroy(int $id): void
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$unit = UnitOption::where('tenant_id', $tenantId)
->where('id', $id)
->first();
if (! $unit) {
throw new NotFoundHttpException(__('error.not_found'));
}
$unit->update(['deleted_by' => $userId]);
$unit->delete();
}
}