From bd81eebf07ef8d7331e82b31603780e1f75287e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sat, 14 Mar 2026 11:52:52 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[=EC=83=81=ED=92=88=EA=B4=80=EB=A6=AC]?= =?UTF-8?q?=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=EB=B3=84=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=80=20=EA=B0=9C=EB=B0=9C=EB=B9=84/=EC=B5=9C=EC=A0=80=20?= =?UTF-8?q?=EA=B5=AC=EB=8F=85=EB=A3=8C=20=EC=84=A4=EC=A0=95=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 카테고리 관리에서 최저 개발비, 최저 구독료 설정 가능 - 상품 추가/수정 시 최저가 이하 입력 차단 (서버 검증) - 상품 목록에 최저가 안내 배너 표시 (경고 아이콘) - 상품 모달에서 실시간 최저가 미달 경고 표시 (빨간 테두리) --- .../Sales/SalesProductController.php | 41 ++++ app/Models/Sales/SalesProductCategory.php | 7 + .../views/sales/products/index.blade.php | 199 +++++++++++++++++- 3 files changed, 237 insertions(+), 10 deletions(-) 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 + {{-- 최저가 안내 --}} + + {{-- 상품 카드 그리드 --}}
@include('sales.products.partials.product-list', ['category' => $currentCategory]) @@ -129,9 +151,17 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:rin -

기본: 개발비의 25%

+

+ + +

@@ -139,8 +169,12 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:rin +
@@ -184,6 +218,72 @@ class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:b
+ {{-- 최저가 설정 모달 --}} +
+
+
+
+ +

최저가 설정

+

+ +
+
+ + + +

설정된 최저가 이하로는 절대 상품 가격을 내릴 수 없습니다. 영업 할인 협상 시에도 이 금액이 하한선이 됩니다.

+
+
+ +
+
+ + +

0 입력 시 제한 없음

+
+
+ + +

0 입력 시 제한 없음

+
+
+ +
+ + +
+
+
+
+ {{-- 카테고리 관리 모달 --}}
카테고리 관리
@foreach($categories as $category) -
-
-
{{ $category->name }}
-
{{ $category->code }} / {{ $category->base_storage }}
-
-
- {{ $category->products->count() }}개 상품 +
+
+
+
{{ $category->name }}
+
{{ $category->code }} / {{ $category->base_storage }}
+
+
+ {{ $category->products->count() }}개 상품 + +
+ @if($category->min_development_fee > 0 || $category->min_subscription_fee > 0) +
+ @if($category->min_development_fee > 0) + 최저 개발비: ₩{{ number_format($category->min_development_fee) }} + @endif + @if($category->min_subscription_fee > 0) + 최저 구독료: ₩{{ number_format($category->min_subscription_fee) }} + @endif +
+ @endif
@endforeach
@@ -243,12 +362,22 @@ function productManager() { currentCategory: '{{ $currentCategory?->code ?? '' }}', categoryName: '{{ $currentCategory?->name ?? '' }}', baseStorage: '{{ $currentCategory?->base_storage ?? '100GB' }}', + currentMinDevFee: {{ $currentCategory?->min_development_fee ?? 0 }}, + currentMinSubFee: {{ $currentCategory?->min_subscription_fee ?? 0 }}, categories: @json($categories), showProductModal: false, showCategoryModal: false, + showMinFeeModal: false, editingProduct: null, + minFeeForm: { + categoryId: null, + categoryName: '', + min_development_fee: 0, + min_subscription_fee: 0, + }, + productForm: { code: '', name: '', @@ -273,6 +402,8 @@ function productManager() { if (cat) { this.categoryName = cat.name; this.baseStorage = cat.base_storage; + this.currentMinDevFee = Number(cat.min_development_fee) || 0; + this.currentMinSubFee = Number(cat.min_subscription_fee) || 0; } htmx.ajax('GET', '{{ route("sales.products.list") }}?category=' + code, { target: '#product-list', @@ -410,6 +541,54 @@ function productManager() { this.productForm.development_fee = fee; // 개발비 자동 계산 (개발비의 25%) this.productForm.registration_fee = Math.floor(fee * 0.25); + }, + + openMinFeeModal(categoryId, categoryName, minDevFee, minSubFee) { + this.minFeeForm = { + categoryId: categoryId, + categoryName: categoryName, + min_development_fee: Number(minDevFee) || 0, + min_subscription_fee: Number(minSubFee) || 0, + }; + this.showMinFeeModal = true; + }, + + async saveMinFees() { + try { + const response = await fetch('{{ url("sales/products/categories") }}/' + this.minFeeForm.categoryId, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content, + }, + body: JSON.stringify({ + min_development_fee: this.minFeeForm.min_development_fee, + min_subscription_fee: this.minFeeForm.min_subscription_fee, + }), + }); + const result = await response.json(); + if (result.success) { + this.showMinFeeModal = false; + // 카테고리 데이터 갱신 + const cat = this.categories.find(c => c.id === this.minFeeForm.categoryId); + if (cat) { + cat.min_development_fee = this.minFeeForm.min_development_fee; + cat.min_subscription_fee = this.minFeeForm.min_subscription_fee; + } + // 현재 선택된 카테고리면 상단 안내 갱신 + if (cat && cat.code === this.currentCategory) { + this.currentMinDevFee = this.minFeeForm.min_development_fee; + this.currentMinSubFee = this.minFeeForm.min_subscription_fee; + } + location.reload(); + } else { + alert(result.message || '저장에 실패했습니다.'); + } + } catch (error) { + console.error(error); + alert('저장 중 오류가 발생했습니다.'); + } } }; }