diff --git a/app/Http/Controllers/Sales/SalesProductController.php b/app/Http/Controllers/Sales/SalesProductController.php
index a6f5fe0b..46e77dbd 100644
--- a/app/Http/Controllers/Sales/SalesProductController.php
+++ b/app/Http/Controllers/Sales/SalesProductController.php
@@ -68,6 +68,13 @@ public function store(Request $request): JsonResponse
'is_required' => 'boolean',
]);
+ // 최저가 검증
+ $category = SalesProductCategory::findOrFail($validated['category_id']);
+ $minFeeErrors = $this->validateMinFees($category, $validated);
+ if ($minFeeErrors) {
+ return response()->json(['success' => false, 'message' => $minFeeErrors], 422);
+ }
+
// 코드 중복 체크
$exists = SalesProduct::where('category_id', $validated['category_id'])
->where('code', $validated['code'])
@@ -115,6 +122,14 @@ public function update(Request $request, int $id): JsonResponse
'is_active' => 'boolean',
]);
+ // 최저가 검증
+ $category = $product->category;
+ $checkData = array_merge($product->toArray(), $validated);
+ $minFeeErrors = $this->validateMinFees($category, $checkData);
+ if ($minFeeErrors) {
+ return response()->json(['success' => false, 'message' => $minFeeErrors], 422);
+ }
+
$product->update($validated);
return response()->json([
@@ -224,6 +239,8 @@ public function updateCategory(Request $request, int $id): JsonResponse
'name' => 'sometimes|string|max:100',
'description' => 'nullable|string',
'base_storage' => 'nullable|string|max:20',
+ 'min_development_fee' => 'nullable|numeric|min:0',
+ 'min_subscription_fee' => 'nullable|numeric|min:0',
'is_active' => 'boolean',
]);
@@ -259,6 +276,30 @@ public function deleteCategory(int $id): JsonResponse
]);
}
+ // ==================== 내부 헬퍼 ====================
+
+ /**
+ * 최저가 검증
+ */
+ private function validateMinFees(SalesProductCategory $category, array $data): ?string
+ {
+ $errors = [];
+
+ if ($category->min_development_fee > 0 && isset($data['registration_fee'])) {
+ if ($data['registration_fee'] < $category->min_development_fee) {
+ $errors[] = '개발비(할인가)는 최저 개발비 ₩'.number_format($category->min_development_fee).' 이상이어야 합니다.';
+ }
+ }
+
+ if ($category->min_subscription_fee > 0 && isset($data['subscription_fee'])) {
+ if ($data['subscription_fee'] < $category->min_subscription_fee) {
+ $errors[] = '월 구독료는 최저 구독료 ₩'.number_format($category->min_subscription_fee).' 이상이어야 합니다.';
+ }
+ }
+
+ return $errors ? implode(' ', $errors) : null;
+ }
+
// ==================== API (영업 시나리오용) ====================
/**
diff --git a/app/Models/Sales/SalesProductCategory.php b/app/Models/Sales/SalesProductCategory.php
index eca14dac..a7c89b72 100644
--- a/app/Models/Sales/SalesProductCategory.php
+++ b/app/Models/Sales/SalesProductCategory.php
@@ -14,6 +14,8 @@
* @property string $name
* @property string|null $description
* @property string $base_storage
+ * @property float $min_development_fee
+ * @property float $min_subscription_fee
* @property int $display_order
* @property bool $is_active
*/
@@ -22,6 +24,7 @@ class SalesProductCategory extends Model
use SoftDeletes;
protected $connection = 'codebridge';
+
protected $table = 'sales_product_categories';
protected $fillable = [
@@ -29,11 +32,15 @@ class SalesProductCategory extends Model
'name',
'description',
'base_storage',
+ 'min_development_fee',
+ 'min_subscription_fee',
'display_order',
'is_active',
];
protected $casts = [
+ 'min_development_fee' => 'decimal:2',
+ 'min_subscription_fee' => 'decimal:2',
'display_order' => 'integer',
'is_active' => 'boolean',
];
diff --git a/resources/views/sales/products/index.blade.php b/resources/views/sales/products/index.blade.php
index 9fddee97..2993df89 100644
--- a/resources/views/sales/products/index.blade.php
+++ b/resources/views/sales/products/index.blade.php
@@ -67,6 +67,28 @@ class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white b
+ {{-- 최저가 안내 --}}
+
+ 최저가 설정 (이 금액 이하로 절대 내릴 수 없습니다)
기본: 개발비의 25%
++ + 최저 개발비 ₩ 이상 필수 + + + 기본: 개발비의 25% + +
최저 구독료 ₩ 이상 필수
+설정된 최저가 이하로는 절대 상품 가격을 내릴 수 없습니다. 영업 할인 협상 시에도 이 금액이 하한선이 됩니다.
+0 입력 시 제한 없음
+0 입력 시 제한 없음
+