fix: [sales] 무료 체험 기간을 1주일(7일)로 변경

- 가격 시뮬레이터: 1/2/3/6개월 선택 → 없음/1주일 토글로 변경
- promoFreeMonths → promoFreeTrial (boolean)으로 변환
- 연 구독료 계산: 1주일분(7/30) 차감 방식 적용
- 데모 테넌트: free_months → free_trial 필드 변경
- 계약 컨트롤러: validation 규칙 업데이트
This commit is contained in:
김보곤
2026-03-15 11:13:07 +09:00
parent 0fc4ab3e0d
commit c7c2fb1f2d
6 changed files with 62 additions and 50 deletions

View File

@@ -150,7 +150,7 @@ public function store(Request $request)
'promo_dev_amount' => 'nullable|integer|min:0|max:5000000',
'promo_dev_waive' => 'nullable|in:0,1',
'promo_sub_percent' => 'nullable|integer|min:0|max:50',
'promo_free_months' => 'nullable|integer|in:0,1,2,3,6',
'promo_free_trial' => 'nullable|in:0,1',
'promo_note' => 'nullable|string|max:200',
]);
@@ -173,7 +173,7 @@ public function store(Request $request)
'dev_discount_amount' => (int) $request->input('promo_dev_amount', 0),
'dev_waive' => $request->input('promo_dev_waive') === '1',
'sub_discount_percent' => (int) $request->input('promo_sub_percent', 0),
'free_months' => (int) $request->input('promo_free_months', 0),
'free_trial' => $request->input('promo_free_trial') === '1',
'note' => $request->input('promo_note', ''),
'applied_at' => now()->toDateTimeString(),
'applied_by_user_id' => auth()->id(),

View File

@@ -35,7 +35,7 @@ public function saveProducts(Request $request): JsonResponse
'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.free_trial' => 'nullable|boolean',
'promotion.note' => 'nullable|string|max:200',
]);

View File

@@ -9,7 +9,7 @@
promoDevAmount: 0,
promoDevWaive: false,
promoSubPercent: 0,
promoFreeMonths: 0,
promoFreeTrial: false,
promoNote: '',
// 개발비 할인 최대값
@@ -35,7 +35,7 @@
parts.push('개발비 ' + Number(this.promoDevAmount).toLocaleString() + '원 할인');
}
if (this.promoSubPercent > 0) parts.push('구독료 ' + this.promoSubPercent + '% 할인');
if (this.promoFreeMonths > 0) parts.push('무료 ' + this.promoFreeMonths + '개월');
if (this.promoFreeTrial) parts.push('무료 체험 1주일');
return parts.length ? parts.join(' / ') : '없음';
},
@@ -55,7 +55,7 @@
// 프로모션 적용 여부
get hasPromo() {
return this.promoDevWaive || this.promoDevPercent > 0 || this.promoDevAmount > 0 || this.promoSubPercent > 0 || this.promoFreeMonths > 0;
return this.promoDevWaive || this.promoDevPercent > 0 || this.promoDevAmount > 0 || this.promoSubPercent > 0 || this.promoFreeTrial;
}
}">
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity" onclick="closeModal()"></div>
@@ -217,17 +217,20 @@ class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-e
</div>
</div>
<!-- 무료 기간 -->
<!-- 무료 체험 (1주일) -->
<div class="bg-gray-50 rounded-lg p-4 space-y-2">
<h4 class="text-sm font-semibold text-gray-700">무료 사용 기간</h4>
<h4 class="text-sm font-semibold text-gray-700">무료 체험 (1주일)</h4>
<div class="flex gap-2">
<template x-for="m in [0, 1, 2, 3, 6]" :key="m">
<button type="button" @click="promoFreeMonths = m"
:class="promoFreeMonths === m ? 'bg-violet-600 text-white' : 'bg-white text-gray-600 border border-gray-300'"
class="px-3 py-1.5 rounded-lg text-xs font-medium transition">
<span x-text="m === 0 ? '없음' : m + '개월'"></span>
</button>
</template>
<button type="button" @click="promoFreeTrial = false"
:class="!promoFreeTrial ? 'bg-violet-600 text-white' : 'bg-white text-gray-600 border border-gray-300'"
class="px-3 py-1.5 rounded-lg text-xs font-medium transition">
없음
</button>
<button type="button" @click="promoFreeTrial = true"
:class="promoFreeTrial ? 'bg-violet-600 text-white' : 'bg-white text-gray-600 border border-gray-300'"
class="px-3 py-1.5 rounded-lg text-xs font-medium transition">
1주일 (7)
</button>
</div>
</div>
@@ -258,7 +261,7 @@ class="w-full px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2
<input type="hidden" name="promo_dev_amount" :value="promoDevAmount">
<input type="hidden" name="promo_dev_waive" :value="promoDevWaive ? '1' : '0'">
<input type="hidden" name="promo_sub_percent" :value="promoSubPercent">
<input type="hidden" name="promo_free_months" :value="promoFreeMonths">
<input type="hidden" name="promo_free_trial" :value="promoFreeTrial ? '1' : '0'">
<input type="hidden" name="promo_note" :value="promoNote">
</div>
</template>

View File

@@ -122,11 +122,11 @@
{{ ($promo['sub_discount_percent'] ?? 0) > 0 ? $promo['sub_discount_percent'] . '%' : '없음' }}
</span>
</div>
{{-- 무료 기간 --}}
{{-- 무료 체험 --}}
<div class="flex justify-between bg-white rounded px-3 py-2">
<span class="text-sm text-gray-600">무료 사용 기간</span>
<span class="text-sm font-bold {{ ($promo['free_months'] ?? 0) > 0 ? 'text-violet-700' : 'text-gray-400' }}">
{{ ($promo['free_months'] ?? 0) > 0 ? $promo['free_months'] . '개월' : '없음' }}
<span class="text-sm text-gray-600">무료 체험</span>
<span class="text-sm font-bold {{ !empty($promo['free_trial']) ? 'text-violet-700' : 'text-gray-400' }}">
{{ !empty($promo['free_trial']) ? '1주일(7일)' : '없음' }}
</span>
</div>
{{-- 메모 --}}

View File

@@ -490,7 +490,7 @@ function productSelection() {
promoDevAmount: {{ $savedPromotion['dev_discount_amount'] ?? 0 }},
promoDevWaive: {{ !empty($savedPromotion['dev_waive']) ? 'true' : 'false' }},
promoSubPercent: {{ $savedPromotion['sub_discount_percent'] ?? 0 }},
promoFreeMonths: {{ $savedPromotion['free_months'] ?? 0 }},
promoFreeTrial: {{ !empty($savedPromotion['free_trial']) ? 'true' : 'false' }},
promoNote: '{{ addslashes($savedPromotion['note'] ?? '') }}',
isSelected(id) {
@@ -713,7 +713,7 @@ function productSelection() {
dev_discount_amount: this.promoDevAmount,
dev_waive: this.promoDevWaive,
sub_discount_percent: this.promoSubPercent,
free_months: this.promoFreeMonths,
free_trial: this.promoFreeTrial,
note: this.promoNote,
};
}

View File

@@ -620,13 +620,13 @@ class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-70
<div class="bg-white rounded-xl p-5 border border-gray-100">
<h3 class="font-bold text-gray-900 mb-3">3. 무료 체험 기간</h3>
<p class="text-sm text-gray-700 leading-relaxed mb-2">
서비스 시작 <strong>처음 개월간 구독료를 면제</strong> 있습니다.
서비스 시작 <strong> 1주일(7) 구독료를 면제</strong> 있습니다.
</p>
<div class="bg-gray-50 rounded-lg p-4 text-sm space-y-2">
<p class="text-gray-600">: 3개월 무료 체험 설정 </p>
<p class="text-gray-600"> 1~3: 구독료 0 / 4~12: 구독료 정상 부과</p>
<p class="text-gray-600"> 1년차 비용 = 개발비 + <strong>9개월</strong> 구독료</p>
<p class="text-gray-500 text-xs mt-1">선택 가능: 없음, 1개월, 2개월, 3개월, 6개월</p>
<p class="text-gray-600">: 무료 체험 적용 </p>
<p class="text-gray-600"> 7: 구독료 0 / 8일째부터: 구독료 정상 부과</p>
<p class="text-gray-600"> 1년차 비용 = 개발비 + <strong>11.75개월</strong> 구독료</p>
<p class="text-gray-500 text-xs mt-1">선택 가능: 없음, 1주일(7)</p>
</div>
</div>
@@ -977,22 +977,27 @@ class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-o
</template>
</div>
{{-- 무료 체험 기간 --}}
{{-- 무료 체험 기간 (1주일 고정) --}}
<div class="border-t border-gray-100 pt-4">
<label class="block text-sm font-medium text-gray-700 mb-2">무료 체험 기간</label>
<label class="block text-sm font-medium text-gray-700 mb-2">무료 체험 (1주일)</label>
<div class="flex gap-2 flex-wrap">
<template x-for="m in [0, 1, 2, 3, 6]" :key="m">
<button type="button" x-on:click="promoFreeMonths = m"
class="px-3 py-1.5 text-xs rounded-lg transition-colors border"
:class="promoFreeMonths === m
? 'bg-orange-500 text-white border-orange-500'
: 'bg-white text-gray-600 border-gray-200 hover:border-orange-300'">
<span x-text="m === 0 ? '없음' : m + '개월'"></span>
</button>
</template>
<button type="button" x-on:click="promoFreeTrial = false"
class="px-3 py-1.5 text-xs rounded-lg transition-colors border"
:class="!promoFreeTrial
? 'bg-orange-500 text-white border-orange-500'
: 'bg-white text-gray-600 border-gray-200 hover:border-orange-300'">
없음
</button>
<button type="button" x-on:click="promoFreeTrial = true"
class="px-3 py-1.5 text-xs rounded-lg transition-colors border"
:class="promoFreeTrial
? 'bg-orange-500 text-white border-orange-500'
: 'bg-white text-gray-600 border-gray-200 hover:border-orange-300'">
1주일 (7)
</button>
</div>
<p class="text-xs text-gray-400 mt-1" x-show="promoFreeMonths > 0">
<span x-text="promoFreeMonths"></span>개월 구독료가 면제됩니다
<p class="text-xs text-gray-400 mt-1" x-show="promoFreeTrial">
7일간 구독료가 면제됩니다
</p>
</div>
@@ -1098,9 +1103,9 @@ class="w-full py-2 text-xs text-gray-500 bg-gray-50 hover:bg-gray-100 rounded-lg
<span class="font-semibold text-gray-900" x-text="formatCurrency(finalSubscriptionFee()) + '/월'"></span>
</div>
{{-- 무료 체험 기간 안내 --}}
<div class="flex justify-between items-center text-xs" x-show="promoFreeMonths > 0">
<div class="flex justify-between items-center text-xs" x-show="promoFreeTrial">
<span class="text-orange-500">무료 체험</span>
<span class="text-orange-600 font-medium" x-text="'첫 ' + promoFreeMonths + '개월 무료'"></span>
<span class="text-orange-600 font-medium"> 1주일(7) 무료</span>
</div>
{{-- 구독료 --}}
<div class="flex justify-between items-center text-sm">
@@ -1116,9 +1121,9 @@ class="w-full py-2 text-xs text-gray-500 bg-gray-50 hover:bg-gray-100 rounded-lg
<span class="font-bold text-xl text-emerald-700" x-text="formatCurrency(totalFirstYearCost())"></span>
</div>
<p class="text-xs text-gray-400 text-right mt-1">
개발비 + <span x-text="12 - promoFreeMonths"></span>개월 구독료 (VAT 별도)
<template x-if="promoFreeMonths > 0">
<span class="text-orange-500"> | <span x-text="promoFreeMonths"></span>개월 무료 적용</span>
개발비 + 12개월 구독료 (VAT 별도)
<template x-if="promoFreeTrial">
<span class="text-orange-500"> | 1주일 무료 체험 적용</span>
</template>
</p>
{{-- 프로모션 미적용 대비 절감액 --}}
@@ -1289,7 +1294,7 @@ function buildRequiredSelected(catId) {
promoRegistrationAmount: 0,
promoWaiveRegistration: false,
promoSubscriptionPercent: 0,
promoFreeMonths: 0,
promoFreeTrial: false,
promoNote: '',
// --- 카테고리 선택 (상호 배타) ---
@@ -1418,7 +1423,7 @@ function buildRequiredSelected(catId) {
return this.promoWaiveRegistration
|| this.promoRegistrationAmount > 0
|| this.promoSubscriptionPercent > 0
|| this.promoFreeMonths > 0;
|| this.promoFreeTrial;
},
promoDevDiscountMax() {
@@ -1481,8 +1486,12 @@ function buildRequiredSelected(catId) {
},
annualSubscriptionFee() {
const paidMonths = 12 - this.promoFreeMonths;
return this.finalSubscriptionFee() * paidMonths;
const annual = this.finalSubscriptionFee() * 12;
if (this.promoFreeTrial) {
// 1주일(7일) 무료: 월 구독료의 7/30 차감
return Math.round(annual - this.finalSubscriptionFee() * 7 / 30);
}
return annual;
},
totalFirstYearCost() {
@@ -1521,7 +1530,7 @@ function buildRequiredSelected(catId) {
this.promoRegistrationAmount = 0;
this.promoWaiveRegistration = false;
this.promoSubscriptionPercent = 0;
this.promoFreeMonths = 0;
this.promoFreeTrial = false;
this.promoNote = '';
},