전체적인 수당 시뮬레이터 로직을 하드코딩된 방식에서 salesConfig 데이터 기반의 동적 방식으로 전면 개편

This commit is contained in:
2026-01-05 05:01:48 +09:00
parent 43a8c47ad1
commit baa560e261

View File

@@ -5094,11 +5094,8 @@
const [editingItem, setEditingItem] = useState(null);
const isOperator = selectedRole === '운영자';
// 1차 선택: 선택모델, 공사관리, 공정/정부지원사업
const [selectedSelectModels, setSelectedSelectModels] = useState(false);
const [selectedConstruction, setSelectedConstruction] = useState(false);
const [selectedProcessGov, setSelectedProcessGov] = useState(false);
// 1차 선택: 동적 패키지 관리
const [selectedPackageIds, setSelectedPackageIds] = useState([]);
// 2차 선택: 선택모델의 세부 모델들
const [selectedModels, setSelectedModels] = useState([]);
@@ -5123,9 +5120,6 @@
}, []);
const packageTypes = salesConfig.package_types || [];
const selectModelsPackage = packageTypes.find(p => p.id === 'select_models');
const constructionPackage = packageTypes.find(p => p.id === 'construction_management');
const processGovPackage = packageTypes.find(p => p.id === 'process_government');
// DB 가격 정보와 기본 설정 병합
const getItemPrice = (itemType, itemId, defaultJoinFee, defaultSubFee) => {
@@ -5148,44 +5142,36 @@
let totalManagerCommission = 0;
let totalEducatorCommission = 0;
// 선택모델의 세부 모델들 합산
if (selectedSelectModels && selectModelsPackage) {
selectedModels.forEach(modelId => {
const model = selectModelsPackage.models.find(m => m.id === modelId);
if (model) {
const price = getItemPrice('model', modelId, model.join_fee, model.subscription_fee);
packageTypes.forEach(pkg => {
if (pkg.id === 'select_models') {
if (selectedPackageIds.includes(pkg.id)) {
(pkg.models || []).forEach(modelIdOrObj => {
// package_types의 models가 ID 배열일수도, 객체 배열일수도 있음 (company_info.php에선 객체 배열)
const model = typeof modelIdOrObj === 'string'
? pkg.models.find(m => m.id === modelIdOrObj)
: modelIdOrObj;
if (model && selectedModels.includes(model.id)) {
const price = getItemPrice('model', model.id, model.join_fee, model.subscription_fee);
totalJoinFee += price.join_fee || 0;
const rates = model.commission_rates || { seller: { join: 0.20 }, manager: { join: 0.05 }, educator: { join: 0 } };
totalSellerCommission += (price.join_fee * (rates.seller?.join || 0.20));
totalManagerCommission += (price.join_fee * (rates.manager?.join || 0.05));
}
});
}
} else {
if (selectedPackageIds.includes(pkg.id)) {
const price = getItemPrice('package', pkg.id, pkg.join_fee, pkg.subscription_fee);
totalJoinFee += price.join_fee || 0;
// 가입비에 대한 수당만 계산: 판매자 20%, 관리자 5%, 메뉴제작 협업수당 별도
const rates = model.commission_rates || { seller: { join: 0.20 }, manager: { join: 0.05 }, educator: { join: 0 } };
totalSellerCommission += (price.join_fee * (rates.seller.join || 0.20));
totalManagerCommission += (price.join_fee * (rates.manager.join || 0.05));
totalEducatorCommission += 0; // 운영팀 별도 산정
const rates = pkg.commission_rates || { seller: { join: 0.20 }, manager: { join: 0.05 }, educator: { join: 0 } };
totalSellerCommission += (price.join_fee * (rates.seller?.join || 0.20));
totalManagerCommission += (price.join_fee * (rates.manager?.join || 0.05));
}
});
}
// 공사관리 패키지
if (selectedConstruction && constructionPackage) {
const price = getItemPrice('package', 'construction_management', constructionPackage.join_fee, constructionPackage.subscription_fee);
totalJoinFee += price.join_fee || 0;
const rates = constructionPackage.commission_rates || { seller: { join: 0.20 }, manager: { join: 0.05 }, educator: { join: 0 } };
totalSellerCommission += (price.join_fee * (rates.seller.join || 0.20));
totalManagerCommission += (price.join_fee * (rates.manager.join || 0.05));
totalEducatorCommission += 0;
}
// 공정/정부지원사업 패키지
if (selectedProcessGov && processGovPackage) {
const price = getItemPrice('package', 'process_government', processGovPackage.join_fee, processGovPackage.subscription_fee);
totalJoinFee += price.join_fee || 0;
const rates = processGovPackage.commission_rates || { seller: { join: 0.20 }, manager: { join: 0.05 }, educator: { join: 0 } };
totalSellerCommission += (price.join_fee * (rates.seller.join || 0.20));
totalManagerCommission += (price.join_fee * (rates.manager.join || 0.05));
totalEducatorCommission += 0;
}
}
});
const totalCommission = totalSellerCommission + totalManagerCommission + totalEducatorCommission;
const totalRevenue = totalJoinFee; // 구독료 제거
@@ -5201,6 +5187,18 @@
);
};
const handlePackageToggle = (pkgId) => {
setSelectedPackageIds(prev => {
const next = prev.includes(pkgId)
? prev.filter(id => id !== pkgId)
: [...prev, pkgId];
if (pkgId === 'select_models' && prev.includes(pkgId)) {
setSelectedModels([]);
}
return next;
});
};
const handleEditPrice = (itemType, itemId, itemName, subName, defaultJoinFee, defaultSubFee) => {
const key = `${itemType}_${itemId}`;
const currentPrice = pricingData[key] || { join_fee: defaultJoinFee, subscription_fee: defaultSubFee };
@@ -5279,129 +5277,40 @@
<div>
<label className="block text-sm font-medium text-slate-700 mb-3">패키지 선택</label>
<div className="space-y-3">
{/* 선택모델 */}
<div className="flex items-start gap-3 p-4 border border-slate-200 rounded-lg hover:border-blue-300 transition-colors">
<input
type="checkbox"
id="select_models"
checked={selectedSelectModels}
onChange={(e) => {
setSelectedSelectModels(e.target.checked);
if (!e.target.checked) {
setSelectedModels([]);
}
}}
className="mt-1 w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500"
/>
<label htmlFor="select_models" className="flex-1 cursor-pointer">
<div className="font-medium text-slate-900">선택모델</div>
<div className="text-xs text-slate-500 mt-1">여러 모델을 선택할 있습니다</div>
</label>
</div>
{/* 공사관리 */}
{constructionPackage && (() => {
const price = getItemPrice('package', 'construction_management', constructionPackage.join_fee, constructionPackage.subscription_fee);
return (
<div className="flex items-start gap-3 p-4 border border-slate-200 rounded-lg hover:border-blue-300 transition-colors">
<input
type="checkbox"
id="construction_management"
checked={selectedConstruction}
onChange={(e) => setSelectedConstruction(e.target.checked)}
className="mt-1 w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500"
/>
<label htmlFor="construction_management" className="flex-1 cursor-pointer">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-slate-900">공사관리</div>
<div className="text-xs text-slate-500 mt-1">패키지</div>
<div className="text-xs text-blue-600 mt-1">
가입비: {formatCurrency(price.join_fee)} / 구독료: {formatCurrency(price.subscription_fee)}
</div>
</div>
{isOperator && (
<button
onClick={(e) => {
e.stopPropagation();
handleEditPrice('package', 'construction_management', '공사관리', '패키지', constructionPackage.join_fee, constructionPackage.subscription_fee);
}}
className="p-1.5 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
title="가격 설정"
>
<LucideIcon name="settings" className="w-4 h-4" />
</button>
)}
</div>
</label>
</div>
);
})()}
{/* 공정/정부지원사업 */}
{processGovPackage && (() => {
const price = getItemPrice('package', 'process_government', processGovPackage.join_fee, processGovPackage.subscription_fee);
return (
<div className="flex items-start gap-3 p-4 border border-slate-200 rounded-lg hover:border-blue-300 transition-colors">
<input
type="checkbox"
id="process_government"
checked={selectedProcessGov}
onChange={(e) => setSelectedProcessGov(e.target.checked)}
className="mt-1 w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500"
/>
<label htmlFor="process_government" className="flex-1 cursor-pointer">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-slate-900">공정/정부지원사업</div>
<div className="text-xs text-slate-500 mt-1">패키지</div>
<div className="text-xs text-blue-600 mt-1">
가입비: {formatCurrency(price.join_fee)} / 구독료: {formatCurrency(price.subscription_fee)}
</div>
</div>
{isOperator && (
<button
onClick={(e) => {
e.stopPropagation();
handleEditPrice('package', 'process_government', '공정/정부지원사업', '패키지', processGovPackage.join_fee, processGovPackage.subscription_fee);
}}
className="p-1.5 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
title="가격 설정"
>
<LucideIcon name="settings" className="w-4 h-4" />
</button>
)}
</div>
</label>
</div>
);
})()}
</div>
</div>
{/* 2차 선택: 선택모델의 세부 모델들 */}
{selectedSelectModels && selectModelsPackage && (
<div className="border-t border-slate-200 pt-6">
<label className="block text-sm font-medium text-slate-700 mb-3">선택모델 세부 항목</label>
<div className="space-y-2 max-h-60 overflow-y-auto">
{selectModelsPackage.models.map(model => {
const price = getItemPrice('model', model.id, model.join_fee, model.subscription_fee);
<div className="space-y-3">
{packageTypes.map(pkg => {
if (pkg.id === 'select_models') {
return (
<div key={model.id} className="flex items-start gap-3 p-3 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors">
<div key={pkg.id} className="flex items-start gap-3 p-4 border border-slate-200 rounded-lg hover:border-blue-300 transition-colors">
<input
type="checkbox"
id={model.id}
checked={selectedModels.includes(model.id)}
onChange={() => handleModelToggle(model.id)}
id={pkg.id}
checked={selectedPackageIds.includes(pkg.id)}
onChange={() => handlePackageToggle(pkg.id)}
className="mt-1 w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500"
/>
<label htmlFor={model.id} className="flex-1 cursor-pointer">
<label htmlFor={pkg.id} className="flex-1 cursor-pointer">
<div className="font-medium text-slate-900">{pkg.name}</div>
<div className="text-xs text-slate-500 mt-1">여러 모델을 선택할 있습니다</div>
</label>
</div>
);
} else {
const price = getItemPrice('package', pkg.id, pkg.join_fee, pkg.subscription_fee);
return (
<div key={pkg.id} className="flex items-start gap-3 p-4 border border-slate-200 rounded-lg hover:border-blue-300 transition-colors">
<input
type="checkbox"
id={pkg.id}
checked={selectedPackageIds.includes(pkg.id)}
onChange={() => handlePackageToggle(pkg.id)}
className="mt-1 w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500"
/>
<label htmlFor={pkg.id} className="flex-1 cursor-pointer">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-slate-900 text-sm">{model.name}</div>
{model.sub_name && (
<div className="text-xs text-slate-500 mt-0.5">{model.sub_name}</div>
)}
<div className="font-medium text-slate-900">{pkg.name}</div>
<div className="text-xs text-slate-500 mt-1">{pkg.sub_name || '패키지'}</div>
<div className="text-xs text-blue-600 mt-1">
가입비: {formatCurrency(price.join_fee)} / 구독료: {formatCurrency(price.subscription_fee)}
</div>
@@ -5410,9 +5319,9 @@
<button
onClick={(e) => {
e.stopPropagation();
handleEditPrice('model', model.id, model.name, model.sub_name, model.join_fee, model.subscription_fee);
handleEditPrice('package', pkg.id, pkg.name, pkg.sub_name, pkg.join_fee, pkg.subscription_fee);
}}
className="p-1.5 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors ml-2"
className="p-1.5 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
title="가격 설정"
>
<LucideIcon name="settings" className="w-4 h-4" />
@@ -5422,10 +5331,63 @@
</label>
</div>
);
})}
</div>
}
})}
</div>
)}
</div>
</div>
{/* 2차 선택: 선택모델의 세부 모델들 */}
{selectedPackageIds.includes('select_models') && (() => {
const selectModelsPkg = packageTypes.find(p => p.id === 'select_models');
if (!selectModelsPkg || !selectModelsPkg.models) return null;
return (
<div className="border-t border-slate-200 pt-6">
<label className="block text-sm font-medium text-slate-700 mb-3">선택모델 세부 항목</label>
<div className="space-y-2 max-h-60 overflow-y-auto">
{selectModelsPkg.models.map(model => {
const price = getItemPrice('model', model.id, model.join_fee, model.subscription_fee);
return (
<div key={model.id} className="flex items-start gap-3 p-3 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors">
<input
type="checkbox"
id={model.id}
checked={selectedModels.includes(model.id)}
onChange={() => handleModelToggle(model.id)}
className="mt-1 w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500"
/>
<label htmlFor={model.id} className="flex-1 cursor-pointer">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-slate-900 text-sm">{model.name}</div>
{model.sub_name && (
<div className="text-xs text-slate-500 mt-0.5">{model.sub_name}</div>
)}
<div className="text-xs text-blue-600 mt-1">
가입비: {formatCurrency(price.join_fee)} / 구독료: {formatCurrency(price.subscription_fee)}
</div>
</div>
{isOperator && (
<button
onClick={(e) => {
e.stopPropagation();
handleEditPrice('model', model.id, model.name, model.sub_name, model.join_fee, model.subscription_fee);
}}
className="p-1.5 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors ml-2"
title="가격 설정"
>
<LucideIcon name="settings" className="w-4 h-4" />
</button>
)}
</div>
</label>
</div>
);
})}
</div>
</div>
);
})()}
</div>
</div>