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:
123
CURRENT_WORKS.md
123
CURRENT_WORKS.md
@@ -1,3 +1,126 @@
|
||||
## 2025-11-20 (수) - ItemMaster Phase 3 API 구현 (부가 기능)
|
||||
|
||||
### 주요 작업
|
||||
- Phase 3 부가 기능 8개 API 엔드포인트 구현
|
||||
- 커스텀 탭, 단위 옵션 관리 기능 추가
|
||||
- SAM API Development Rules 준수 (Service-First, ApiResponse, i18n)
|
||||
|
||||
### 추가된 파일
|
||||
|
||||
#### Controllers (2개)
|
||||
1. **app/Http/Controllers/Api/V1/ItemMaster/CustomTabController.php**
|
||||
- index(), store(), update(), destroy(), reorder()
|
||||
- 커스텀 탭 관리 및 순서 변경
|
||||
|
||||
2. **app/Http/Controllers/Api/V1/ItemMaster/UnitOptionController.php**
|
||||
- index(), store(), destroy()
|
||||
- 단위 옵션 관리 (Update 없음)
|
||||
|
||||
#### Services (2개)
|
||||
1. **app/Services/ItemMaster/CustomTabService.php**
|
||||
- CRUD + reorder
|
||||
- columnSetting 관계 Eager Loading
|
||||
- order_no 자동 계산
|
||||
|
||||
2. **app/Services/ItemMaster/UnitOptionService.php**
|
||||
- index, store, destroy만 구현
|
||||
- label 정렬
|
||||
|
||||
#### FormRequests (3개)
|
||||
1. **app/Http/Requests/ItemMaster/CustomTabStoreRequest.php**
|
||||
- label (required), icon, is_default
|
||||
|
||||
2. **app/Http/Requests/ItemMaster/CustomTabUpdateRequest.php**
|
||||
- 모든 필드 sometimes
|
||||
|
||||
3. **app/Http/Requests/ItemMaster/UnitOptionStoreRequest.php**
|
||||
- label (required), value (required)
|
||||
|
||||
### 수정된 파일
|
||||
|
||||
1. **routes/api.php**
|
||||
- ItemMaster 관련 use 문 2개 추가
|
||||
- 8개 엔드포인트 추가:
|
||||
- GET/POST/PUT/DELETE /custom-tabs, PUT /custom-tabs/reorder
|
||||
- GET/POST/DELETE /unit-options
|
||||
|
||||
### 작업 내용
|
||||
|
||||
#### API 엔드포인트 (8개)
|
||||
1. ✅ GET `/custom-tabs` - 커스텀 탭 목록
|
||||
2. ✅ POST `/custom-tabs` - 커스텀 탭 생성
|
||||
3. ✅ PUT `/custom-tabs/{id}` - 커스텀 탭 수정
|
||||
4. ✅ DELETE `/custom-tabs/{id}` - 커스텀 탭 삭제
|
||||
5. ✅ PUT `/custom-tabs/reorder` - 커스텀 탭 순서 변경
|
||||
6. ✅ GET `/unit-options` - 단위 옵션 목록
|
||||
7. ✅ POST `/unit-options` - 단위 옵션 생성
|
||||
8. ✅ DELETE `/unit-options/{id}` - 단위 옵션 삭제
|
||||
|
||||
#### 기술적 특징
|
||||
|
||||
**Service-First 패턴**:
|
||||
- Controller는 DI + ApiResponse::handle()만 사용
|
||||
- 모든 비즈니스 로직은 Service에 구현
|
||||
- Service extends Service (tenantId(), apiUserId() 활용)
|
||||
|
||||
**Multi-tenant 지원**:
|
||||
- 모든 Service 메서드에서 tenantId() 검증
|
||||
- BelongsToTenant 스코프 자동 적용
|
||||
- Soft Delete시 tenant_id 검증
|
||||
|
||||
**실시간 저장**:
|
||||
- 모든 CUD 작업 즉시 처리
|
||||
- order_no 자동 계산 (CustomTab)
|
||||
- reorder는 배열로 한 번에 처리
|
||||
|
||||
**i18n 메시지**:
|
||||
- __('message.fetched'), __('message.created')
|
||||
- __('message.updated'), __('message.deleted')
|
||||
- __('message.reordered'), __('error.not_found')
|
||||
|
||||
### 검증 결과
|
||||
|
||||
**라우트 테스트**:
|
||||
```bash
|
||||
php artisan route:list --path=item-master
|
||||
# 결과: 32개 엔드포인트 정상 등록 (Phase 1: 13개 + Phase 2: 11개 + Phase 3: 8개)
|
||||
```
|
||||
|
||||
**Pint 검사**:
|
||||
```bash
|
||||
./vendor/bin/pint --test [7개 파일]
|
||||
# 결과: 7 files PASS
|
||||
```
|
||||
|
||||
### 완료 상태
|
||||
|
||||
**ItemMaster API 전체 구현 완료**:
|
||||
- Phase 1 (핵심): 13개 엔드포인트 ✅
|
||||
- Phase 2 (확장): 11개 엔드포인트 ✅
|
||||
- Phase 3 (부가): 8개 엔드포인트 ✅
|
||||
- **총 32개 엔드포인트 구현 완료**
|
||||
|
||||
**다음 작업**:
|
||||
- Swagger 문서화 (app/Swagger/v1/ItemMasterApi.php)
|
||||
- API 테스트 케이스 작성
|
||||
- 프론트엔드 연동
|
||||
|
||||
### Git 커밋
|
||||
```bash
|
||||
git commit [hash]
|
||||
feat: ItemMaster Phase 3 API 구현 (부가 기능 8개 엔드포인트)
|
||||
|
||||
- Controller 2개, Service 2개, FormRequest 3개 생성
|
||||
- Routes 등록 (커스텀 탭, 단위 옵션)
|
||||
- Service-First, Multi-tenant, Soft Delete
|
||||
- 라우트 테스트 및 Pint 검사 통과
|
||||
- ItemMaster API 전체 32개 엔드포인트 구현 완료
|
||||
|
||||
8 files changed, 400+ insertions(+)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2025-11-20 (수) - ItemMaster Phase 2 API 구현 (확장 기능)
|
||||
|
||||
### 주요 작업
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
22
app/Http/Requests/ItemMaster/CustomTabStoreRequest.php
Normal file
22
app/Http/Requests/ItemMaster/CustomTabStoreRequest.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
22
app/Http/Requests/ItemMaster/CustomTabUpdateRequest.php
Normal file
22
app/Http/Requests/ItemMaster/CustomTabUpdateRequest.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
21
app/Http/Requests/ItemMaster/UnitOptionStoreRequest.php
Normal file
21
app/Http/Requests/ItemMaster/UnitOptionStoreRequest.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
111
app/Services/ItemMaster/CustomTabService.php
Normal file
111
app/Services/ItemMaster/CustomTabService.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
61
app/Services/ItemMaster/UnitOptionService.php
Normal file
61
app/Services/ItemMaster/UnitOptionService.php
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
use App\Http\Controllers\Api\V1\EstimateController;
|
||||
use App\Http\Controllers\Api\V1\FileStorageController;
|
||||
use App\Http\Controllers\Api\V1\FolderController;
|
||||
use App\Http\Controllers\Api\V1\ItemMaster\CustomTabController;
|
||||
use App\Http\Controllers\Api\V1\ItemMaster\ItemBomItemController;
|
||||
use App\Http\Controllers\Api\V1\ItemMaster\ItemFieldController;
|
||||
use App\Http\Controllers\Api\V1\ItemMaster\ItemMasterController;
|
||||
@@ -26,6 +27,7 @@
|
||||
use App\Http\Controllers\Api\V1\ItemMaster\ItemPageController;
|
||||
use App\Http\Controllers\Api\V1\ItemMaster\ItemSectionController;
|
||||
use App\Http\Controllers\Api\V1\ItemMaster\SectionTemplateController;
|
||||
use App\Http\Controllers\Api\V1\ItemMaster\UnitOptionController;
|
||||
use App\Http\Controllers\Api\V1\MaterialController;
|
||||
use App\Http\Controllers\Api\V1\MenuController;
|
||||
use App\Http\Controllers\Api\V1\ModelSetController;
|
||||
@@ -515,6 +517,18 @@
|
||||
Route::post('/master-fields', [ItemMasterFieldController::class, 'store'])->name('v1.item-master.master-fields.store');
|
||||
Route::put('/master-fields/{id}', [ItemMasterFieldController::class, 'update'])->name('v1.item-master.master-fields.update');
|
||||
Route::delete('/master-fields/{id}', [ItemMasterFieldController::class, 'destroy'])->name('v1.item-master.master-fields.destroy');
|
||||
|
||||
// 커스텀 탭
|
||||
Route::get('/custom-tabs', [CustomTabController::class, 'index'])->name('v1.item-master.custom-tabs.index');
|
||||
Route::post('/custom-tabs', [CustomTabController::class, 'store'])->name('v1.item-master.custom-tabs.store');
|
||||
Route::put('/custom-tabs/{id}', [CustomTabController::class, 'update'])->name('v1.item-master.custom-tabs.update');
|
||||
Route::delete('/custom-tabs/{id}', [CustomTabController::class, 'destroy'])->name('v1.item-master.custom-tabs.destroy');
|
||||
Route::put('/custom-tabs/reorder', [CustomTabController::class, 'reorder'])->name('v1.item-master.custom-tabs.reorder');
|
||||
|
||||
// 단위 옵션
|
||||
Route::get('/unit-options', [UnitOptionController::class, 'index'])->name('v1.item-master.unit-options.index');
|
||||
Route::post('/unit-options', [UnitOptionController::class, 'store'])->name('v1.item-master.unit-options.store');
|
||||
Route::delete('/unit-options/{id}', [UnitOptionController::class, 'destroy'])->name('v1.item-master.unit-options.destroy');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user