diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index eef8081..c28f716 100644 --- a/CURRENT_WORKS.md +++ b/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 구현 (확장 기능) ### 주요 작업 diff --git a/app/Http/Controllers/Api/V1/ItemMaster/CustomTabController.php b/app/Http/Controllers/Api/V1/ItemMaster/CustomTabController.php new file mode 100644 index 0000000..37c69a9 --- /dev/null +++ b/app/Http/Controllers/Api/V1/ItemMaster/CustomTabController.php @@ -0,0 +1,71 @@ +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')); + } +} diff --git a/app/Http/Controllers/Api/V1/ItemMaster/UnitOptionController.php b/app/Http/Controllers/Api/V1/ItemMaster/UnitOptionController.php new file mode 100644 index 0000000..b721e4e --- /dev/null +++ b/app/Http/Controllers/Api/V1/ItemMaster/UnitOptionController.php @@ -0,0 +1,47 @@ +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')); + } +} diff --git a/app/Http/Requests/ItemMaster/CustomTabStoreRequest.php b/app/Http/Requests/ItemMaster/CustomTabStoreRequest.php new file mode 100644 index 0000000..a89dd36 --- /dev/null +++ b/app/Http/Requests/ItemMaster/CustomTabStoreRequest.php @@ -0,0 +1,22 @@ + 'required|string|max:255', + 'icon' => 'nullable|string|max:100', + 'is_default' => 'nullable|boolean', + ]; + } +} diff --git a/app/Http/Requests/ItemMaster/CustomTabUpdateRequest.php b/app/Http/Requests/ItemMaster/CustomTabUpdateRequest.php new file mode 100644 index 0000000..dd7ff05 --- /dev/null +++ b/app/Http/Requests/ItemMaster/CustomTabUpdateRequest.php @@ -0,0 +1,22 @@ + 'sometimes|string|max:255', + 'icon' => 'sometimes|nullable|string|max:100', + 'is_default' => 'sometimes|nullable|boolean', + ]; + } +} diff --git a/app/Http/Requests/ItemMaster/UnitOptionStoreRequest.php b/app/Http/Requests/ItemMaster/UnitOptionStoreRequest.php new file mode 100644 index 0000000..0ef63b1 --- /dev/null +++ b/app/Http/Requests/ItemMaster/UnitOptionStoreRequest.php @@ -0,0 +1,21 @@ + 'required|string|max:100', + 'value' => 'required|string|max:50', + ]; + } +} diff --git a/app/Services/ItemMaster/CustomTabService.php b/app/Services/ItemMaster/CustomTabService.php new file mode 100644 index 0000000..34cae30 --- /dev/null +++ b/app/Services/ItemMaster/CustomTabService.php @@ -0,0 +1,111 @@ +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, + ]); + } + } +} diff --git a/app/Services/ItemMaster/UnitOptionService.php b/app/Services/ItemMaster/UnitOptionService.php new file mode 100644 index 0000000..aa4456a --- /dev/null +++ b/app/Services/ItemMaster/UnitOptionService.php @@ -0,0 +1,61 @@ +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(); + } +} diff --git a/routes/api.php b/routes/api.php index 50dc628..509b737 100644 --- a/routes/api.php +++ b/routes/api.php @@ -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'); }); });