diff --git a/app/Http/Controllers/Sales/SalesContractController.php b/app/Http/Controllers/Sales/SalesContractController.php
index ed9fe6e9..92115ba0 100644
--- a/app/Http/Controllers/Sales/SalesContractController.php
+++ b/app/Http/Controllers/Sales/SalesContractController.php
@@ -30,6 +30,13 @@ public function saveProducts(Request $request): JsonResponse
'products.*.category_id' => 'required|exists:codebridge.sales_product_categories,id',
'products.*.registration_fee' => 'required|numeric|min:0',
'products.*.subscription_fee' => 'required|numeric|min:0',
+ 'promotion' => 'nullable|array',
+ 'promotion.dev_discount_type' => 'nullable|string|in:percent,amount',
+ 'promotion.dev_discount_amount' => 'nullable|numeric|min:0',
+ 'promotion.dev_waive' => 'nullable|boolean',
+ 'promotion.sub_discount_percent' => 'nullable|numeric|min:0|max:50',
+ 'promotion.free_months' => 'nullable|integer|in:0,1,2,3,6',
+ 'promotion.note' => 'nullable|string|max:200',
]);
// tenant_id 또는 prospect_id 중 하나는 필수
@@ -79,6 +86,16 @@ public function saveProducts(Request $request): JsonResponse
$totalRegistrationFee = SalesContractProduct::where('management_id', $management->id)
->sum('registration_fee');
$management->update(['total_registration_fee' => $totalRegistrationFee]);
+
+ // 프로모션 저장
+ if (! empty($validated['promotion'])) {
+ $opts = $management->options ?? [];
+ $opts['promotion'] = array_merge($validated['promotion'], [
+ 'applied_at' => now()->toDateTimeString(),
+ 'applied_by_user_id' => auth()->id(),
+ ]);
+ $management->update(['options' => $opts]);
+ }
});
return response()->json([
diff --git a/app/Models/Sales/SalesTenantManagement.php b/app/Models/Sales/SalesTenantManagement.php
index 4e405239..27c3ce26 100644
--- a/app/Models/Sales/SalesTenantManagement.php
+++ b/app/Models/Sales/SalesTenantManagement.php
@@ -38,6 +38,7 @@ class SalesTenantManagement extends Model
use SoftDeletes;
protected $connection = 'codebridge';
+
protected $table = 'sales_tenant_managements';
protected $fillable = [
@@ -72,6 +73,7 @@ class SalesTenantManagement extends Model
'balance_paid_date',
'balance_status',
'total_registration_fee',
+ 'options',
];
protected $casts = [
@@ -94,6 +96,7 @@ class SalesTenantManagement extends Model
'balance_amount' => 'decimal:2',
'balance_paid_date' => 'date',
'total_registration_fee' => 'decimal:2',
+ 'options' => 'array',
];
/**
diff --git a/resources/views/sales/modals/partials/product-selection.blade.php b/resources/views/sales/modals/partials/product-selection.blade.php
index 96c81301..6f365ca7 100644
--- a/resources/views/sales/modals/partials/product-selection.blade.php
+++ b/resources/views/sales/modals/partials/product-selection.blade.php
@@ -20,6 +20,7 @@
$management = SalesTenantManagement::where('tenant_id', $entity->id)->first();
}
$managementId = $management?->id;
+ $savedPromotion = $management?->options['promotion'] ?? null;
// 이미 선택된 상품들 조회 (management_id 기반)
$selectedProducts = [];
@@ -208,6 +209,140 @@ class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-i
@endforeach
+ {{-- 영업 재량 할인/프로모션 --}}
+
+
+
+
+
+
영업 재량 할인/프로모션
+
파트너 재량으로 추가 할인 적용
+
+
+
+
+
+ {{-- 개발비 할인 --}}
+
+
+
+
+
+ (100% 할인 적용)
+
+
+
+ {{-- 구독료 할인 --}}
+
+
+ 구독료 할인
+
+
+
+
+ 0%
+
+
+
+
+ {{-- 무료 기간 --}}
+
+
무료 사용 기간
+
+
+
+
+
+
+ 첫 개월 구독료가 면제됩니다
+
+
+
+ {{-- 프로모션 메모 --}}
+
+
+
+
+
+ {{-- 프로모션 요약 --}}
+
+
적용 프로모션 요약
+
+ ✓ 개발비 전액 면제
+
+
+ ✓ 개발비
+
+
+
+ ✓ 구독료
+
+
+ ✓ 구독료
+
+
+
+ {{-- 초기화 버튼 --}}
+
+
+
+
+
+
{{-- 합계 영역 (전체 카테고리 합산) --}}
@@ -218,18 +353,55 @@ class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-i
+ {{-- 프로모션 할인 금액 --}}
+
+
+ 개발비 할인
+
+
+
{{-- 1년차 총 비용 --}}
-
-
1년차 총 비용
-
+
+
+
+ 1년차 총 비용
+
+ (개월 무료 적용)
+
+
+
+
+
+
+ 프로모션 총 절감액
+
+
+
+ {{-- 프로모션 메모 --}}
+
+
+ 프로모션:
+
+
+
@@ -312,6 +484,15 @@ function productSelection() {
entityId: {{ $entity->id }},
managementId: {{ $managementId ?? 'null' }},
+ // 프로모션
+ showPromo: {{ $savedPromotion ? 'true' : 'false' }},
+ promoDevType: '{{ $savedPromotion['dev_discount_type'] ?? 'percent' }}',
+ promoDevAmount: {{ $savedPromotion['dev_discount_amount'] ?? 0 }},
+ promoDevWaive: {{ !empty($savedPromotion['dev_waive']) ? 'true' : 'false' }},
+ promoSubPercent: {{ $savedPromotion['sub_discount_percent'] ?? 0 }},
+ promoFreeMonths: {{ $savedPromotion['free_months'] ?? 0 }},
+ promoNote: '{{ addslashes($savedPromotion['note'] ?? '') }}',
+
isSelected(id) {
return this.selected.hasOwnProperty(id);
},
@@ -330,6 +511,7 @@ function productSelection() {
[id]: { regFee: p.regFee, subFee: p.subFee }
};
}
+ this.$nextTick(() => this.clampPromoValues());
},
getRegFee(id) {
@@ -355,6 +537,7 @@ function productSelection() {
}
this.selected = { ...this.selected, [id]: update };
+ this.$nextTick(() => this.clampPromoValues());
},
toggleLinkedPricing() {
@@ -403,6 +586,95 @@ function productSelection() {
return '₩' + Number(value).toLocaleString();
},
+ // --- 프로모션 계산 ---
+ hasAnyPromo() {
+ return this.promoDevWaive || this.promoDevAmount > 0 || this.promoSubPercent > 0 || this.promoFreeMonths > 0;
+ },
+
+ promoDevDiscountMax() {
+ const totalReg = this.totalRegFee();
+ // 최저 개발비 합산 (min_development_fee가 있는 상품들의 최저가 합산)
+ let totalMin = 0;
+ Object.keys(this.selected).forEach(id => {
+ const p = productMap[id];
+ if (p) totalMin += p.minDevFee;
+ });
+ const maxDiscount = totalReg - totalMin;
+ if (this.promoDevType === 'percent') {
+ if (totalReg <= 0) return 0;
+ return Math.min(50, Math.floor(maxDiscount / totalReg * 100 / 5) * 5);
+ }
+ return Math.max(0, maxDiscount);
+ },
+
+ promoSubMaxPercent() {
+ // 최저 구독료 기준으로 최대 할인율 계산
+ const totalSub = this.totalSubFee();
+ if (totalSub <= 0) return 0;
+ let totalMinSub = 0;
+ Object.keys(this.selected).forEach(id => {
+ const p = productMap[id];
+ if (p) totalMinSub += p.minSubFee;
+ });
+ const maxPercent = Math.floor((1 - totalMinSub / totalSub) * 100 / 5) * 5;
+ return Math.min(50, Math.max(0, maxPercent));
+ },
+
+ promoRegDiscount() {
+ const totalReg = this.totalRegFee();
+ if (this.promoDevWaive) {
+ let totalMin = 0;
+ Object.keys(this.selected).forEach(id => {
+ const p = productMap[id];
+ if (p) totalMin += p.minDevFee;
+ });
+ return Math.max(0, totalReg - totalMin);
+ }
+ if (this.promoDevAmount <= 0) return 0;
+ if (this.promoDevType === 'percent') {
+ return Math.floor(totalReg * this.promoDevAmount / 100);
+ }
+ return Math.min(this.promoDevAmount, totalReg);
+ },
+
+ promoAdjustedRegFee() {
+ return Math.max(0, this.totalRegFee() - this.promoRegDiscount());
+ },
+
+ promoAdjustedSubFee() {
+ const base = this.totalSubFee();
+ if (this.promoSubPercent <= 0) return base;
+ return Math.floor(base * (100 - this.promoSubPercent) / 100);
+ },
+
+ promoFirstYearTotal() {
+ const regFee = this.promoAdjustedRegFee();
+ const subFee = this.promoAdjustedSubFee();
+ const paidMonths = 12 - this.promoFreeMonths;
+ return regFee + subFee * paidMonths;
+ },
+
+ promoTotalSaving() {
+ const originalTotal = this.totalRegFee() + this.totalSubFee() * 12;
+ return originalTotal - this.promoFirstYearTotal();
+ },
+
+ resetPromo() {
+ this.promoDevType = 'percent';
+ this.promoDevAmount = 0;
+ this.promoDevWaive = false;
+ this.promoSubPercent = 0;
+ this.promoFreeMonths = 0;
+ this.promoNote = '';
+ },
+
+ clampPromoValues() {
+ const devMax = this.promoDevDiscountMax();
+ if (this.promoDevAmount > devMax) this.promoDevAmount = devMax;
+ const subMax = this.promoSubMaxPercent();
+ if (this.promoSubPercent > subMax) this.promoSubPercent = subMax;
+ },
+
async saveSelection() {
this.saving = true;
try {
@@ -434,6 +706,18 @@ function productSelection() {
}
requestData.management_id = this.managementId;
+ // 프로모션 정보 포함
+ if (this.hasAnyPromo()) {
+ requestData.promotion = {
+ dev_discount_type: this.promoDevType,
+ dev_discount_amount: this.promoDevAmount,
+ dev_waive: this.promoDevWaive,
+ sub_discount_percent: this.promoSubPercent,
+ free_months: this.promoFreeMonths,
+ note: this.promoNote,
+ };
+ }
+
const response = await fetch('/sales/contracts/products', {
method: 'POST',
headers: {