feat: [가격시뮬레이터] 개발비-구독료 연동 조절 기능 추가
- 토글 스위치로 연동 ON/OFF - 연동 ON: 개발비 슬라이더 조정 시 구독료가 비율 유지하며 자동 연동 - 각 상품의 원래 개발비(할인가):구독료 비율 기반 계산 - 최저 구독료 이하로는 연동되지 않도록 제한 - 슬라이더 아래 연동 구독료 실시간 표시 - 우측 요약 패널에 원래/연동 구독료 비교 표시 - 상품 목록에서도 연동 구독료 변화 표시
This commit is contained in:
@@ -736,6 +736,37 @@ class="w-4 h-4 rounded border-gray-300 text-emerald-600 focus:ring-emerald-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 개발비-구독료 연동 --}}
|
||||
<div class="bg-white rounded-xl shadow-sm p-4 mb-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg" :class="linkedPricing ? 'bg-blue-100' : 'bg-gray-100'">
|
||||
<svg class="w-5 h-5" :class="linkedPricing ? 'text-blue-600' : 'text-gray-400'" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm font-semibold text-gray-900">개발비-구독료 연동</span>
|
||||
<p class="text-xs text-gray-500">개발비를 조정하면 구독료가 비율에 맞춰 자동 연동됩니다</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" x-on:click="toggleLinkedPricing()"
|
||||
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
|
||||
:class="linkedPricing ? 'bg-blue-600' : 'bg-gray-200'">
|
||||
<span class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||
:class="linkedPricing ? 'translate-x-5' : 'translate-x-0'"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div x-show="linkedPricing" x-collapse class="mt-3 p-3 bg-blue-50 rounded-lg text-xs text-blue-700">
|
||||
<div class="flex items-start gap-2">
|
||||
<svg class="w-4 h-4 mt-0.5 shrink-0 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>각 상품의 <strong>개발비(할인가) : 구독료</strong> 비율을 유지합니다. 슬라이더로 개발비를 낮추면 구독료도 같은 비율로 낮아집니다. (최저 구독료 이하로는 내려가지 않습니다)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 선택된 카테고리의 상품 목록 --}}
|
||||
@foreach($categories as $category)
|
||||
<div x-show="selectedCategoryId === {{ $category->id }}" class="bg-white rounded-xl shadow-sm mb-4 overflow-hidden">
|
||||
@@ -777,7 +808,17 @@ class="w-4 h-4 rounded border-gray-300 text-emerald-600 focus:ring-emerald-500"
|
||||
<div class="flex items-center gap-4 mt-2 text-xs text-gray-500">
|
||||
<span>개발비: <strong class="text-gray-700">{{ number_format($product->development_fee) }}원</strong></span>
|
||||
<span>개발비: <strong class="text-indigo-600">{{ number_format($product->registration_fee) }}원</strong></span>
|
||||
<span>구독료: <strong class="text-gray-700">{{ number_format($product->subscription_fee) }}원/월</strong></span>
|
||||
<span>구독료:
|
||||
<template x-if="linkedPricing && isSelected({{ $product->id }}) && getAdjustedSubFee({{ $product->id }}) !== {{ $product->subscription_fee }}">
|
||||
<span>
|
||||
<span class="line-through text-gray-400">{{ number_format($product->subscription_fee) }}원</span>
|
||||
<strong class="text-blue-600 ml-1" x-text="Number(getAdjustedSubFee({{ $product->id }})).toLocaleString() + '원/월'"></strong>
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="!(linkedPricing && isSelected({{ $product->id }}) && getAdjustedSubFee({{ $product->id }}) !== {{ $product->subscription_fee }})">
|
||||
<strong class="text-gray-700">{{ number_format($product->subscription_fee) }}원/월</strong>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{-- 선택 시 가격 조정 슬라이더 --}}
|
||||
@@ -816,6 +857,18 @@ class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-e
|
||||
최저 {{ number_format($product->min_development_fee) }}원 이하 불가
|
||||
</div>
|
||||
@endif
|
||||
{{-- 연동 구독료 표시 --}}
|
||||
<template x-if="linkedPricing">
|
||||
<div class="mt-2 pt-2 border-t border-blue-200 bg-blue-50 rounded px-2 py-1.5 flex items-center justify-between">
|
||||
<span class="text-xs text-blue-600 flex items-center gap-1">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
||||
</svg>
|
||||
연동 구독료
|
||||
</span>
|
||||
<span class="text-xs font-bold text-blue-700" x-text="formatCurrency(getAdjustedSubFee({{ $product->id }})) + '/월'"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -1026,9 +1079,14 @@ class="w-full py-2 text-xs text-gray-500 bg-gray-50 hover:bg-gray-100 rounded-lg
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-100 pt-3 space-y-2">
|
||||
{{-- 원래 구독료 (연동 시에만 표시) --}}
|
||||
<div class="flex justify-between items-center text-sm" x-show="linkedPricing && totalOriginalSubscriptionFee() !== totalSubscriptionFee()">
|
||||
<span class="text-gray-400">월 구독료 (원래)</span>
|
||||
<span class="text-gray-400 line-through" x-text="formatCurrency(totalOriginalSubscriptionFee()) + '/월'"></span>
|
||||
</div>
|
||||
{{-- 월 구독료 --}}
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<span class="text-gray-500">월 구독료 (정가)</span>
|
||||
<span class="text-gray-500" x-text="linkedPricing && totalOriginalSubscriptionFee() !== totalSubscriptionFee() ? '월 구독료 (연동)' : '월 구독료 (정가)'"></span>
|
||||
<span x-text="formatCurrency(totalSubscriptionFee()) + '/월'"
|
||||
:class="promoSubscriptionPercent > 0 ? 'text-gray-400 line-through' : 'font-semibold text-gray-900'"></span>
|
||||
</div>
|
||||
@@ -1223,6 +1281,9 @@ function buildRequiredSelected(catId) {
|
||||
selectedCategoryId: firstCategoryId,
|
||||
selected: buildRequiredSelected(firstCategoryId),
|
||||
|
||||
// 개발비-구독료 연동
|
||||
linkedPricing: false,
|
||||
|
||||
// 프로모션/할인
|
||||
promoRegistrationType: 'percent', // 'percent' | 'amount'
|
||||
promoRegistrationAmount: 0,
|
||||
@@ -1277,10 +1338,28 @@ function buildRequiredSelected(catId) {
|
||||
if (!this.selected[id]) return;
|
||||
const p = productMap[id];
|
||||
const minFee = p ? p.minDevFee : 0;
|
||||
this.selected = {
|
||||
...this.selected,
|
||||
[id]: { ...this.selected[id], adjustedFee: Math.max(minFee, value) }
|
||||
};
|
||||
const clampedFee = Math.max(minFee, value);
|
||||
const update = { ...this.selected[id], adjustedFee: clampedFee };
|
||||
|
||||
// 연동 모드: 개발비 비율에 맞춰 구독료 자동 조정
|
||||
if (this.linkedPricing && p) {
|
||||
const baseRegFee = Number(p.registration_fee) || 1;
|
||||
const baseSub = Number(p.subscription_fee);
|
||||
const ratio = baseSub / baseRegFee;
|
||||
const linkedSub = Math.round(clampedFee * ratio / 10000) * 10000; // 만원 단위 반올림
|
||||
const minSub = p.minSubFee || 0;
|
||||
update.adjustedSubFee = Math.max(minSub, linkedSub);
|
||||
}
|
||||
|
||||
this.selected = { ...this.selected, [id]: update };
|
||||
},
|
||||
|
||||
getAdjustedSubFee(id) {
|
||||
if (!this.selected[id]) return 0;
|
||||
if (this.selected[id].adjustedSubFee !== undefined) {
|
||||
return this.selected[id].adjustedSubFee;
|
||||
}
|
||||
return Number(productMap[id]?.subscription_fee || 0);
|
||||
},
|
||||
|
||||
getDiscountRate(id, devFee) {
|
||||
@@ -1304,7 +1383,8 @@ function buildRequiredSelected(catId) {
|
||||
categoryName: p.categoryName,
|
||||
developmentFee: Number(p.development_fee),
|
||||
adjustedFee: this.selected[id].adjustedFee,
|
||||
subscriptionFee: Number(p.subscription_fee),
|
||||
subscriptionFee: this.getAdjustedSubFee(id),
|
||||
originalSubFee: Number(p.subscription_fee),
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -1322,6 +1402,12 @@ function buildRequiredSelected(catId) {
|
||||
},
|
||||
|
||||
totalSubscriptionFee() {
|
||||
return Object.keys(this.selected).reduce((sum, id) => {
|
||||
return sum + this.getAdjustedSubFee(id);
|
||||
}, 0);
|
||||
},
|
||||
|
||||
totalOriginalSubscriptionFee() {
|
||||
return Object.keys(this.selected).reduce((sum, id) => {
|
||||
return sum + Number(productMap[id].subscription_fee);
|
||||
}, 0);
|
||||
@@ -1486,9 +1572,35 @@ function buildRequiredSelected(catId) {
|
||||
return Number(value).toLocaleString('ko-KR') + '원';
|
||||
},
|
||||
|
||||
toggleLinkedPricing() {
|
||||
this.linkedPricing = !this.linkedPricing;
|
||||
if (this.linkedPricing) {
|
||||
// 연동 ON: 현재 조정된 개발비 기준으로 구독료 재계산
|
||||
Object.keys(this.selected).forEach(id => {
|
||||
const p = productMap[id];
|
||||
if (!p) return;
|
||||
const baseRegFee = Number(p.registration_fee) || 1;
|
||||
const baseSub = Number(p.subscription_fee);
|
||||
const ratio = baseSub / baseRegFee;
|
||||
const linkedSub = Math.round(this.selected[id].adjustedFee * ratio / 10000) * 10000;
|
||||
const minSub = p.minSubFee || 0;
|
||||
this.selected[id] = { ...this.selected[id], adjustedSubFee: Math.max(minSub, linkedSub) };
|
||||
});
|
||||
this.selected = { ...this.selected };
|
||||
} else {
|
||||
// 연동 OFF: 구독료를 원래값으로 복원
|
||||
Object.keys(this.selected).forEach(id => {
|
||||
delete this.selected[id].adjustedSubFee;
|
||||
});
|
||||
this.selected = { ...this.selected };
|
||||
}
|
||||
this.$nextTick(() => this.clampPromoValues());
|
||||
},
|
||||
|
||||
resetAll() {
|
||||
this.signupType = 'individual';
|
||||
this.hasReferrer = false;
|
||||
this.linkedPricing = false;
|
||||
this.selectedCategoryId = firstCategoryId;
|
||||
this.selected = buildRequiredSelected(firstCategoryId);
|
||||
this.resetPromo();
|
||||
|
||||
Reference in New Issue
Block a user