<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enzycept Sales Chart Generator</title>
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
padding: 40px;
}
h1 {
color: #333;
text-align: center;
margin-bottom: 10px;
font-size: 32px;
}
.subtitle {
text-align: center;
color: #666;
margin-bottom: 30px;
font-size: 16px;
}
.upload-section {
background: #f8f9ff;
border-radius: 15px;
padding: 30px;
margin-bottom: 30px;
}
.upload-area {
border: 3px dashed #667eea;
border-radius: 15px;
padding: 40px;
text-align: center;
background: white;
cursor: pointer;
transition: all 0.3s;
}
.upload-area:hover {
background: #f0f2ff;
border-color: #764ba2;
}
.upload-area.dragover {
background: #e8ebff;
border-color: #764ba2;
transform: scale(1.02);
}
.upload-icon {
font-size: 48px;
margin-bottom: 15px;
}
input[type="file"] {
display: none;
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 15px 40px;
border-radius: 10px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
margin-top: 20px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 25px;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.stat-card h3 {
font-size: 14px;
opacity: 0.9;
margin-bottom: 10px;
}
.stat-card .value {
font-size: 28px;
font-weight: bold;
margin-bottom: 5px;
}
.stat-card .detail {
font-size: 14px;
opacity: 0.8;
}
.charts-section {
margin-top: 30px;
}
.chart-container {
background: #f8f9ff;
border-radius: 15px;
padding: 30px;
margin-bottom: 30px;
}
.chart-title {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
text-align: center;
}
canvas {
max-height: 500px;
}
.status {
padding: 15px;
border-radius: 10px;
margin-top: 20px;
text-align: center;
display: none;
}
.status.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status.loading {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 10px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.download-section {
text-align: center;
margin-top: 30px;
}
.download-btn {
background: #2ecc71;
margin: 10px;
display: inline-block;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="container">
<h1>📊 Enzycept Plus Solution</h1>
<p class="subtitle">Sales Chart Generator - 엔지셉트플러스액 3년 매출 분석</p>
<div class="upload-section">
<div class="upload-area" id="uploadArea">
<div class="upload-icon">📁</div>
<h3>Excel 파일을 선택하거나 드래그하세요</h3>
<p style="color: #666; margin-top: 10px;">지원 형식: .xlsx, .xls</p>
</div>
<input type="file" id="fileInput" accept=".xlsx,.xls">
<div style="text-align: center;">
<button class="btn" id="generateBtn" disabled>차트 생성</button>
</div>
<div class="status" id="status"></div>
</div>
<div id="resultsSection" class="hidden">
<h2 style="margin-bottom: 20px;">📈 분석 결과</h2>
<div class="stats-grid" id="statsGrid"></div>
<div class="charts-section">
<div class="chart-container">
<h3 class="chart-title">월별 매출액 추이</h3>
<canvas id="salesChart"></canvas>
</div>
<div class="chart-container">
<h3 class="chart-title">월별 판매수량 추이</h3>
<canvas id="quantityChart"></canvas>
</div>
<div class="chart-container">
<h3 class="chart-title">연간 매출 비교</h3>
<canvas id="annualChart"></canvas>
</div>
</div>
<div class="download-section">
<button class="btn download-btn" onclick="downloadCharts()">📥 차트 다운로드 (PNG)</button>
</div>
</div>
</div>
<script>
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const generateBtn = document.getElementById('generateBtn');
const status = document.getElementById('status');
const resultsSection = document.getElementById('resultsSection');
let selectedFile = null;
let statsData = null;
let charts = {};
// 파일 선택 이벤트
uploadArea.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
// 드래그 앤 드롭
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
handleFile(e.dataTransfer.files[0]);
});
function handleFile(file) {
if (!file) return;
if (!file.name.match(/\.(xlsx|xls)$/)) {
showStatus('Excel 파일(.xlsx, .xls)을 선택해주세요.', 'error');
return;
}
selectedFile = file;
uploadArea.innerHTML = `
<div class="upload-icon">✅</div>
<h3>${file.name}</h3>
<p style="color: #666; margin-top: 10px;">파일 크기: ${(file.size / 1024).toFixed(2)} KB</p>
`;
generateBtn.disabled = false;
hideStatus();
}
generateBtn.addEventListener('click', processFile);
function processFile() {
if (!selectedFile) return;
showStatus('데이터 분석 중...', 'loading');
generateBtn.disabled = true;
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, {type: 'array'});
// '3년 추이' 시트 찾기
const sheetName = workbook.SheetNames.find(name => name.includes('3년 추이'));
if (!sheetName) {
throw new Error("'3년 추이' 시트를 찾을 수 없습니다.");
}
const worksheet = workbook.Sheets[sheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, {header: 1});
// 데이터 추출 및 처리
statsData = processExcelData(jsonData);
// 결과 표시
displayResults();
showStatus('차트 생성 완료!', 'success');
} catch (error) {
showStatus('오류: ' + error.message, 'error');
generateBtn.disabled = false;
}
};
reader.readAsArrayBuffer(selectedFile);
}
function processExcelData(data) {
const years = [2023, 2024, 2025];
const months = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'];
const salesData = {};
const quantityData = {};
// 데이터 추출 (row 3, 4, 5가 2023, 2024, 2025)
years.forEach((year, yearIdx) => {
const rowIdx = yearIdx + 3;
salesData[year] = [];
quantityData[year] = [];
for (let monthIdx = 0; monthIdx < 12; monthIdx++) {
const colIdx = 1 + (monthIdx * 2);
const sales = parseFloat(data[rowIdx][colIdx]) || 0;
const qty = parseFloat(data[rowIdx][colIdx + 1]) || 0;
salesData[year].push(sales);
quantityData[year].push(qty);
}
});
// 연간 합계
const annualSales = {};
const annualQty = {};
years.forEach(year => {
annualSales[year] = salesData[year].reduce((a, b) => a + b, 0);
annualQty[year] = quantityData[year].reduce((a, b) => a + b, 0);
});
// 성장률
const growth2024 = ((annualSales[2024] - annualSales[2023]) / annualSales[2023] * 100).toFixed(1);
const growth2025 = ((annualSales[2025] - annualSales[2024]) / annualSales[2024] * 100).toFixed(1);
return {
salesData,
quantityData,
annualSales,
annualQty,
growth2024,
growth2025
};
}
function displayResults() {
resultsSection.classList.remove('hidden');
// 통계 카드 생성
const statsGrid = document.getElementById('statsGrid');
statsGrid.innerHTML = `
<div class="stat-card">
<h3>2023년 총 매출</h3>
<div class="value">₩${statsData.annualSales[2023].toLocaleString()}</div>
<div class="detail">${statsData.annualQty[2023].toLocaleString()} 개</div>
</div>
<div class="stat-card">
<h3>2024년 총 매출</h3>
<div class="value">₩${statsData.annualSales[2024].toLocaleString()}</div>
<div class="detail">성장률: ${statsData.growth2024 > 0 ? '+' : ''}${statsData.growth2024}%</div>
</div>
<div class="stat-card">
<h3>2025년 총 매출</h3>
<div class="value">₩${statsData.annualSales[2025].toLocaleString()}</div>
<div class="detail">성장률: ${statsData.growth2025 > 0 ? '+' : ''}${statsData.growth2025}%</div>
</div>
`;
// 차트 생성
createCharts();
// 결과 섹션으로 스크롤
resultsSection.scrollIntoView({ behavior: 'smooth' });
}
function createCharts() {
const months = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'];
const years = [2023, 2024, 2025];
const colors = ['#3498db', '#2ecc71', '#e74c3c'];
// 기존 차트 제거
Object.values(charts).forEach(chart => chart && chart.destroy());
// 월별 매출액 차트
const salesCtx = document.getElementById('salesChart').getContext('2d');
charts.sales = new Chart(salesCtx, {
type: 'bar',
data: {
labels: months,
datasets: years.map((year, idx) => ({
label: `${year}년`,
data: statsData.salesData[year].map(v => v / 1000000),
backgroundColor: colors[idx] + 'CC',
borderColor: colors[idx],
borderWidth: 2
}))
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { display: true, position: 'top' },
title: { display: false }
},
scales: {
y: {
beginAtZero: true,
title: { display: true, text: '매출액 (백만원)' }
}
}
}
});
// 월별 수량 차트
const qtyCtx = document.getElementById('quantityChart').getContext('2d');
charts.quantity = new Chart(qtyCtx, {
type: 'line',
data: {
labels: months,
datasets: years.map((year, idx) => ({
label: `${year}년`,
data: statsData.quantityData[year],
borderColor: colors[idx],
backgroundColor: colors[idx] + '33',
borderWidth: 3,
fill: true,
tension: 0.4
}))
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { display: true, position: 'top' }
},
scales: {
y: {
beginAtZero: true,
title: { display: true, text: '판매수량 (개)' }
}
}
}
});
// 연간 비교 차트
const annualCtx = document.getElementById('annualChart').getContext('2d');
charts.annual = new Chart(annualCtx, {
type: 'bar',
data: {
labels: years.map(y => `${y}년`),
datasets: [{
label: '연간 매출액',
data: years.map(y => statsData.annualSales[y] / 1000000),
backgroundColor: colors.map(c => c + 'CC'),
borderColor: colors,
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { display: false }
},
scales: {
y: {
beginAtZero: true,
title: { display: true, text: '매출액 (백만원)' }
}
}
}
});
}
function downloadCharts() {
Object.keys(charts).forEach((key, idx) => {
const canvas = charts[key].canvas;
const link = document.createElement('a');
link.download = `enzycept_${key}_chart.png`;
link.href = canvas.toDataURL();
link.click();
});
showStatus('차트 다운로드 완료!', 'success');
}
function showStatus(message, type) {
status.innerHTML = message;
if (type === 'loading') {
status.innerHTML += '<div class="spinner"></div>';
}
status.className = 'status ' + type;
status.style.display = 'block';
}
function hideStatus() {
status.style.display = 'none';
}
</script>
</body>
</html>