From ceb7798c285a91817a4345427472326260cd8931 Mon Sep 17 00:00:00 2001 From: kent Date: Fri, 9 Jan 2026 22:19:11 +0900 Subject: [PATCH] =?UTF-8?q?feat(pricing):=20=EB=8B=A8=EA=B0=80=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20stats,=20bulkDestroy=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /pricing/stats: 단가 통계 조회 (total, draft, finalized, expired) - DELETE /pricing/bulk: 단가 일괄 삭제 (확정된 단가 제외) - PriceBulkDeleteRequest FormRequest 추가 - PricingService.stats(), bulkDestroy() 메서드 구현 Co-Authored-By: Claude --- .../Controllers/Api/V1/PricingController.php | 26 ++++++++ .../Pricing/PriceBulkDeleteRequest.php | 32 ++++++++++ app/Services/PricingService.php | 61 +++++++++++++++++++ routes/api.php | 2 + 4 files changed, 121 insertions(+) create mode 100644 app/Http/Requests/Pricing/PriceBulkDeleteRequest.php diff --git a/app/Http/Controllers/Api/V1/PricingController.php b/app/Http/Controllers/Api/V1/PricingController.php index 99456f0..484e9c0 100644 --- a/app/Http/Controllers/Api/V1/PricingController.php +++ b/app/Http/Controllers/Api/V1/PricingController.php @@ -4,6 +4,7 @@ use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; +use App\Http\Requests\Pricing\PriceBulkDeleteRequest; use App\Http\Requests\Pricing\PriceByItemsRequest; use App\Http\Requests\Pricing\PriceCostRequest; use App\Http\Requests\Pricing\PriceIndexRequest; @@ -124,4 +125,29 @@ public function cost(PriceCostRequest $request) return ['data' => $data, 'message' => __('message.fetched')]; }); } + + /** + * 단가 통계 조회 + */ + public function stats() + { + return ApiResponse::handle(function () { + $data = $this->service->stats(); + + return ['data' => $data, 'message' => __('message.fetched')]; + }); + } + + /** + * 단가 일괄 삭제 + */ + public function bulkDestroy(PriceBulkDeleteRequest $request) + { + return ApiResponse::handle(function () use ($request) { + $validated = $request->validated(); + $deletedCount = $this->service->bulkDestroy($validated['ids']); + + return ['data' => ['deleted_count' => $deletedCount], 'message' => __('message.deleted')]; + }); + } } diff --git a/app/Http/Requests/Pricing/PriceBulkDeleteRequest.php b/app/Http/Requests/Pricing/PriceBulkDeleteRequest.php new file mode 100644 index 0000000..e11c6e8 --- /dev/null +++ b/app/Http/Requests/Pricing/PriceBulkDeleteRequest.php @@ -0,0 +1,32 @@ + 'required|array|min:1', + 'ids.*' => 'required|integer|min:1', + ]; + } + + public function messages(): array + { + return [ + 'ids.required' => __('validation.required', ['attribute' => 'ids']), + 'ids.array' => __('validation.array', ['attribute' => 'ids']), + 'ids.min' => __('validation.min.array', ['attribute' => 'ids', 'min' => 1]), + 'ids.*.required' => __('validation.required', ['attribute' => 'id']), + 'ids.*.integer' => __('validation.integer', ['attribute' => 'id']), + ]; + } +} diff --git a/app/Services/PricingService.php b/app/Services/PricingService.php index 803f5d8..16d022d 100644 --- a/app/Services/PricingService.php +++ b/app/Services/PricingService.php @@ -537,4 +537,65 @@ private function shouldRecalculateSalesPrice(array $data): bool return false; } + + /** + * 단가 통계 조회 + */ + public function stats(): array + { + $tenantId = $this->tenantId(); + + $baseQuery = Price::where('tenant_id', $tenantId); + + $total = (clone $baseQuery)->count(); + $draft = (clone $baseQuery)->where('status', 'draft')->count(); + $finalized = (clone $baseQuery)->where('status', 'finalized')->count(); + $expired = (clone $baseQuery)->where('status', 'expired')->count(); + + return [ + 'total' => $total, + 'draft' => $draft, + 'finalized' => $finalized, + 'expired' => $expired, + ]; + } + + /** + * 단가 일괄 삭제 (soft delete) + */ + public function bulkDestroy(array $ids): int + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($ids, $tenantId, $userId) { + $prices = Price::query() + ->where('tenant_id', $tenantId) + ->whereIn('id', $ids) + ->get(); + + if ($prices->isEmpty()) { + throw new NotFoundHttpException(__('error.not_found')); + } + + $deletedCount = 0; + foreach ($prices as $price) { + // 확정된 단가는 삭제 불가 + if ($price->is_final) { + continue; + } + + // 삭제 전 스냅샷 저장 + $beforeSnapshot = $price->toSnapshot(); + $this->createRevision($price, $beforeSnapshot, $userId, __('message.pricing.deleted')); + + $price->deleted_by = $userId; + $price->save(); + $price->delete(); + $deletedCount++; + } + + return $deletedCount; + }); + } } diff --git a/routes/api.php b/routes/api.php index 8c2ca21..ba455bc 100644 --- a/routes/api.php +++ b/routes/api.php @@ -989,8 +989,10 @@ // Pricing (단가 관리) Route::prefix('pricing')->group(function () { Route::get('', [PricingController::class, 'index'])->name('v1.pricing.index'); // 목록 + Route::get('/stats', [PricingController::class, 'stats'])->name('v1.pricing.stats'); // 통계 Route::get('/cost', [PricingController::class, 'cost'])->name('v1.pricing.cost'); // 원가 조회 Route::post('/by-items', [PricingController::class, 'byItems'])->name('v1.pricing.by-items'); // 품목별 단가 현황 + Route::delete('/bulk', [PricingController::class, 'bulkDestroy'])->name('v1.pricing.bulk-destroy'); // 일괄 삭제 Route::post('', [PricingController::class, 'store'])->name('v1.pricing.store'); // 등록 Route::get('/{id}', [PricingController::class, 'show'])->whereNumber('id')->name('v1.pricing.show'); // 상세 Route::put('/{id}', [PricingController::class, 'update'])->whereNumber('id')->name('v1.pricing.update'); // 수정