feat(pricing): 단가관리 stats, bulkDestroy API 추가

- GET /pricing/stats: 단가 통계 조회 (total, draft, finalized, expired)
- DELETE /pricing/bulk: 단가 일괄 삭제 (확정된 단가 제외)
- PriceBulkDeleteRequest FormRequest 추가
- PricingService.stats(), bulkDestroy() 메서드 구현

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-09 22:19:11 +09:00
parent e6a4bf0870
commit ceb7798c28
4 changed files with 121 additions and 0 deletions

View File

@@ -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')];
});
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests\Pricing;
use Illuminate\Foundation\Http\FormRequest;
class PriceBulkDeleteRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'ids' => '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']),
];
}
}

View File

@@ -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;
});
}
}

View File

@@ -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'); // 수정