Files
sam-kd/salesmangement/index.php
hskwon aca1767eb9 초기 커밋: 5130 레거시 시스템
- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경
- DB 연결 하드코딩 → .env 기반으로 변경
- MySQL strict mode DATE 오류 수정
2025-12-10 20:14:31 +09:00

289 lines
15 KiB
PHP

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>영업 관리 시스템 - CodeBridgeExy</title>
<!-- Fonts: Pretendard -->
<link rel="stylesheet" as="style" crossorigin href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.8/dist/web/static/pretendard.css" />
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Pretendard', 'sans-serif'],
},
colors: {
background: 'rgb(250, 250, 250)',
primary: {
DEFAULT: '#2563eb', // blue-600
foreground: '#ffffff',
},
},
borderRadius: {
'card': '12px',
}
}
}
}
</script>
<!-- React & ReactDOM -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<!-- Babel for JSX -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- Icons: Lucide React (via CDN is tricky, using simple SVG icons or a library wrapper if needed. For now, using text/simple SVGs) -->
<script src="https://unpkg.com/lucide@latest"></script>
</head>
<body class="bg-background text-slate-800 antialiased">
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
// --- Components ---
// 1. Header Component
const Header = ({ companyInfo }) => {
if (!companyInfo) return <div className="h-16 bg-white shadow-sm animate-pulse"></div>;
return (
<header className="bg-white border-b border-gray-100 sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center text-blue-600 font-bold">
{companyInfo.name.substring(0, 1)}
</div>
<h1 className="text-lg font-semibold text-slate-900">{companyInfo.name} 영업 관리</h1>
</div>
<div className="flex items-center gap-4">
<button className="text-sm text-slate-500 hover:text-slate-900">도움말</button>
<div className="w-8 h-8 rounded-full bg-slate-200"></div>
</div>
</div>
</header>
);
};
// 2. Dashboard Card Component
const StatCard = ({ title, value, subtext, icon }) => (
<div className="bg-white rounded-card p-6 shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between mb-4">
<h3 className="text-sm font-medium text-slate-500">{title}</h3>
<div className="p-2 bg-blue-50 rounded-lg text-blue-600">
{icon}
</div>
</div>
<div className="text-2xl font-bold text-slate-900 mb-1">{value}</div>
{subtext && <div className="text-xs text-slate-400">{subtext}</div>}
</div>
);
// 3. Main App Component
const App = () => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
useEffect(() => {
// Fetch Mock Data
fetch('api/company_info.php')
.then(res => res.json())
.then(jsonData => {
setData(jsonData);
setLoading(false);
})
.catch(err => {
console.error("Failed to fetch data:", err);
setLoading(false);
});
}, []);
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
);
}
if (!data) return <div>데이터를 불러올 없습니다.</div>;
return (
<div className="min-h-screen pb-20">
<Header companyInfo={data.company_info} />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
{/* Dashboard Section */}
<section>
<h2 className="text-xl font-bold text-slate-900 mb-6">대시보드</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<StatCard
title="총 매출액"
value="₩ 124,800,000"
subtext="전월 대비 +12%"
icon={<i data-lucide="dollar-sign" className="w-5 h-5"></i>}
/>
<StatCard
title="이번 달 예상 수당"
value="₩ 8,500,000"
subtext="지급 예정일: 25일"
icon={<i data-lucide="wallet" className="w-5 h-5"></i>}
/>
<StatCard
title="활성 계약"
value="14 건"
subtext="신규 2건"
icon={<i data-lucide="file-check" className="w-5 h-5"></i>}
/>
<StatCard
title="평균 구독 유지율"
value="98.5%"
subtext="업계 평균 상회"
icon={<i data-lucide="trending-up" className="w-5 h-5"></i>}
/>
</div>
</section>
{/* Simulator Section */}
<SimulatorSection salesConfig={data.sales_config} />
</main>
</div>
);
};
// 4. Simulator Component
const SimulatorSection = ({ salesConfig }) => {
const [selectedProgramId, setSelectedProgramId] = useState(salesConfig.programs[0]?.id);
const [duration, setDuration] = useState(salesConfig.default_contract_period);
const [customJoinFee, setCustomJoinFee] = useState(null);
const [customSubFee, setCustomSubFee] = useState(null);
const selectedProgram = salesConfig.programs.find(p => p.id === selectedProgramId);
// Calculate Commissions
const joinFee = customJoinFee !== null ? customJoinFee : (selectedProgram?.join_fee || 0);
const subFee = customSubFee !== null ? customSubFee : (selectedProgram?.subscription_fee || 0);
const rates = selectedProgram?.commission_rates || { seller: { join: 0, sub: 0 }, manager: { join: 0, sub: 0 } };
const sellerCommission = (joinFee * rates.seller.join) + (subFee * rates.seller.sub * duration);
const managerCommission = (joinFee * rates.manager.join) + (subFee * rates.manager.sub * duration);
const totalCommission = sellerCommission + managerCommission;
const formatCurrency = (val) => new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'KRW' }).format(val);
return (
<section className="bg-white rounded-card shadow-sm border border-slate-100 overflow-hidden flex flex-col lg:flex-row">
{/* Input Form */}
<div className="p-8 lg:w-1/2 border-b lg:border-b-0 lg:border-r border-slate-100">
<h2 className="text-xl font-bold text-slate-900 mb-6 flex items-center gap-2">
<i data-lucide="calculator" className="w-5 h-5 text-blue-600"></i>
수당 시뮬레이터
</h2>
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">프로그램 선택</label>
<select
className="w-full rounded-lg border-slate-200 border p-3 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
value={selectedProgramId}
onChange={(e) => {
setSelectedProgramId(e.target.value);
setCustomJoinFee(null); // Reset custom values on program change
setCustomSubFee(null);
}}
>
{salesConfig.programs.map(prog => (
<option key={prog.id} value={prog.id}>{prog.name}</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">가입비 ()</label>
<input
type="number"
className="w-full rounded-lg border-slate-200 border p-3 focus:ring-2 focus:ring-blue-500 outline-none"
value={joinFee}
onChange={(e) => setCustomJoinFee(Number(e.target.value))}
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2"> 구독료 ()</label>
<input
type="number"
className="w-full rounded-lg border-slate-200 border p-3 focus:ring-2 focus:ring-blue-500 outline-none"
value={subFee}
onChange={(e) => setCustomSubFee(Number(e.target.value))}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">계약 기간 (개월)</label>
<div className="flex items-center gap-4">
<input
type="range"
min="12"
max="120"
step="12"
className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-blue-600"
value={duration}
onChange={(e) => setDuration(Number(e.target.value))}
/>
<span className="text-sm font-bold text-blue-600 w-16 text-right">{duration}개월</span>
</div>
</div>
</div>
</div>
{/* Result Card */}
<div className="p-8 lg:w-1/2 bg-slate-50 flex flex-col justify-center">
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 p-6 relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-50 rounded-full -mr-16 -mt-16 opacity-50"></div>
<h3 className="text-sm font-semibold text-slate-400 uppercase tracking-wider mb-6">예상 수당 명세서</h3>
<div className="space-y-4 mb-6">
<div className="flex justify-between items-center p-3 bg-slate-50 rounded-lg">
<span className="text-slate-600">판매자 수당</span>
<span className="font-bold text-slate-900">{formatCurrency(sellerCommission)}</span>
</div>
<div className="flex justify-between items-center p-3 bg-slate-50 rounded-lg">
<span className="text-slate-600">관리자 수당</span>
<span className="font-bold text-slate-900">{formatCurrency(managerCommission)}</span>
</div>
</div>
<div className="pt-6 border-t border-dashed border-slate-200">
<div className="flex justify-between items-end">
<span className="text-lg font-bold text-slate-800"> 예상 수당</span>
<span className="text-3xl font-extrabold text-blue-600">{formatCurrency(totalCommission)}</span>
</div>
<p className="text-right text-xs text-slate-400 mt-2">* 세전 금액 기준입니다.</p>
</div>
</div>
</div>
</section>
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
// Initialize Lucide Icons after render
setTimeout(() => {
lucide.createIcons();
}, 100);
</script>
</body>
</html>