<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://unpkg.com/@sgratzl/chartjs-chart-boxplot@3.6.0/build/index.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; background: #f3f4f6; padding: 20px; color: #1f2937; margin: 0; }
/* 헤더 */
header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
h1 { margin: 0; font-size: 1.5rem; color: #111827; }
/* 탭 메뉴 */
.tabs { display: flex; gap: 10px; margin-bottom: 20px; background: white; padding: 8px; border-radius: 12px; }
.tab-btn { flex: 1; padding: 12px; border: none; background: transparent; cursor: pointer; font-weight: 600; color: #6b7280; border-radius: 8px; transition: 0.2s; }
.tab-btn:hover { background: #f9fafb; }
.tab-btn.active { background: #3b82f6; color: white; box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.5); }
/* 컨텐츠 섹션 */
.view-section { display: none; }
.view-section.active { display: block; }
.section-title { font-size: 1.2rem; font-weight: 800; margin: 30px 0 15px 0; border-left: 5px solid #3b82f6; padding-left: 10px; color: #1f2937; }
.card { background: white; border-radius: 16px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin-bottom: 20px; position: relative; }
/* 차트 그리드 */
.chart-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; }
.chart-box { height: 250px; position: relative; }
/* 다운로드 버튼 */
.download-btn {
position: absolute; top: 10px; right: 10px;
background: white; border: 1px solid #e5e7eb; border-radius: 6px;
padding: 4px 8px; font-size: 0.75rem; cursor: pointer; color: #6b7280;
z-index: 10;
}
.download-btn:hover { background: #f3f4f6; color: #3b82f6; }
/* 히트맵 테이블 */
.heatmap-table { width: 100%; border-collapse: collapse; font-size: 0.8rem; text-align: center; }
.heatmap-table th, .heatmap-table td { padding: 8px; border: 1px solid #eee; }
.heatmap-table th { background: #f9fafb; font-weight: 600; }
/* AI 박스 */
.ai-box { background: #eff6ff; border: 1px solid #bfdbfe; padding: 20px; border-radius: 12px; line-height: 1.6; margin-bottom: 20px; }
.ai-header { font-weight: bold; color: #1e40af; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
/* 로딩 */
#loading { display: none; text-align: center; padding: 50px; }
.spinner { border: 4px solid #f3f3f3; border-top: 4px solid #3b82f6; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 0 auto; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
/* 도움말 탭 스타일 */
.guide-box { background: #fff; padding: 20px; border-radius: 12px; margin-bottom: 15px; border-left: 4px solid #10b981; }
.guide-title { font-weight: bold; font-size: 1.1rem; color: #059669; margin-bottom: 5px; }
</style>
</head>
<body>
<header>
<h1>🚀 마케팅 데이터 종합 분석</h1>
<div style="font-size:0.8rem; color:#6b7280;">Live Connected</div>
</header>
<div class="tabs">
<button class="tab-btn active" onclick="switchTab('basic')">📋 기초 통계 (종합)</button>
<button class="tab-btn" onclick="switchTab('guide')">📘 분석 가이드 (Help)</button>
</div>
<div id="loading">
<div class="spinner"></div>
<p style="margin-top:10px; color:#6b7280;">데이터를 분석하고 차트를 생성 중입니다...</p>
</div>
<div id="view-basic" class="view-section">
<div class="card">
<div class="ai-box">
<div class="ai-header">🤖 AI 데이터 인사이트</div>
<div id="ai-basic-report">분석 중...</div>
</div>
</div>
<div class="section-title">1. 데이터 분포 (Distributions)</div>
<div class="chart-grid" id="dist-grid"></div>
<div class="section-title">2. 이상치 확인 (Boxplots)</div>
<div class="chart-grid" id="box-grid"></div>
<div class="section-title">3. 판매량과의 관계 (Scatter vs Product_Sold)</div>
<div class="chart-grid" id="scatter-grid"></div>
<div class="section-title">4. 상관관계 히트맵 (Correlation Matrix)</div>
<div class="card" style="overflow-x:auto;">
<table class="heatmap-table" id="heatmap-table"></table>
</div>
</div>
<div id="view-guide" class="view-section">
<div class="guide-box">
<div class="guide-title">📊 히스토그램 (Histogram)</div>
<p>데이터가 어디에 많이 몰려있는지 보여줍니다. 막대가 높은 구간이 가장 흔한 값입니다. 만약 한쪽으로 치우쳐 있다면(비대칭), 평균값 사용에 주의해야 합니다.</p>
</div>
<div class="guide-box">
<div class="guide-title">📦 박스 플롯 (Box Plot)</div>
<p>데이터의 퍼짐 정도와 '이상치(Outlier)'를 찾습니다. 박스 위아래에 점이 찍혀 있다면, 다른 값들과 동떨어진 특이한 데이터가 있다는 뜻입니다.</p>
</div>
<div class="guide-box">
<div class="guide-title">📉 산점도 (Scatter Plot)</div>
<p>두 변수의 관계를 점으로 찍어 보여줍니다. 점들이 우상향하면 '양의 상관관계(비례)', 우하향하면 '음의 상관관계(반비례)'입니다. 점들이 뭉쳐있을수록 관계가 강합니다.</p>
</div>
<div class="guide-box">
<div class="guide-title">🔗 상관계수 (Correlation Coefficient)</div>
<p><strong>+1에 가까울수록:</strong> 아주 강한 양의 관계 (하나가 늘면 다른 것도 확실히 늠)<br>
<strong>0에 가까울수록:</strong> 관계 없음<br>
<strong>-1에 가까울수록:</strong> 아주 강한 음의 관계 (하나가 늘면 다른 건 줄어듦)</p>
</div>
</div>
<script>
let globalData = null;
// 초기 로딩
window.onload = function() {
document.getElementById('loading').style.display = 'block';
google.script.run.withSuccessHandler(initDashboard).getFullData();
};
function initDashboard(data) {
globalData = data;
document.getElementById('loading').style.display = 'none';
switchTab('basic');
}
function switchTab(tabName) {
document.querySelectorAll('.view-section').forEach(el => el.classList.remove('active'));
document.getElementById('view-' + tabName).classList.add('active');
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
event.target.classList.add('active');
if (tabName === 'basic') renderBasicAnalysis();
}
// === 종합 분석 렌더링 ===
function renderBasicAnalysis() {
const headers = globalData.headers;
const rows = globalData.rows;
const numericHeaders = headers.filter(h => globalData.stats[h]);
const targetVar = 'Product_Sold';
callAI('EDA_SUMMARY', globalData.stats, 'ai-basic-report');
// 1. 분포 (Histograms)
const distGrid = document.getElementById('dist-grid');
distGrid.innerHTML = '';
numericHeaders.forEach(col => {
const div = createChartCard(`dist-${col}`);
distGrid.appendChild(div);
const vals = rows.map(r => r[headers.indexOf(col)]);
drawHistogram(`dist-${col}`, vals, col);
});
// 2. 이상치 (Boxplots)
const boxGrid = document.getElementById('box-grid');
boxGrid.innerHTML = '';
numericHeaders.forEach(col => {
const div = createChartCard(`box-${col}`);
boxGrid.appendChild(div);
const vals = rows.map(r => r[headers.indexOf(col)]);
drawBoxplot(`box-${col}`, vals, col);
});
// 3. 산점도 (Scatter)
const scatterGrid = document.getElementById('scatter-grid');
scatterGrid.innerHTML = '';
numericHeaders.forEach(col => {
if (col === targetVar) return;
const div = createChartCard(`scat-${col}`);
scatterGrid.appendChild(div);
const xVals = rows.map(r => r[headers.indexOf(col)]);
const yVals = rows.map(r => r[headers.indexOf(targetVar)]);
drawScatter(`scat-${col}`, xVals, yVals, col, targetVar);
});
renderHeatmap(numericHeaders, rows, headers);
}
// 차트 카드 생성 (다운로드 버튼 포함)
function createChartCard(canvasId) {
const div = document.createElement('div');
div.className = 'card chart-box';
div.innerHTML = `
<button class="download-btn" onclick="downloadChart('${canvasId}')">📷 저장</button>
<canvas id="${canvasId}"></canvas>
`;
return div;
}
// 차트 이미지 다운로드 함수
function downloadChart(canvasId) {
const link = document.createElement('a');
link.download = canvasId + '.png';
link.href = document.getElementById(canvasId).toDataURL();
link.click();
}
// --- 차트 그리기 헬퍼 함수들 ---
function drawHistogram(canvasId, data, label) {
const ctx = document.getElementById(canvasId).getContext('2d');
const bins = 10;
const min = Math.min(...data);
const max = Math.max(...data);
const step = (max - min) / bins;
let freq = new Array(bins).fill(0);
let labels = [];
for(let i=0; i<bins; i++) labels.push((min + step*i).toFixed(0));
data.forEach(v => {
let bucket = Math.floor((v - min) / step);
if (bucket >= bins) bucket = bins - 1;
freq[bucket]++;
});
new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{ label: label, data: freq, backgroundColor: '#3b82f6', borderRadius: 4 }]
},
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, title: { display: true, text: label } }, scales: { x: { display: false } } }
});
}
function drawBoxplot(canvasId, data, label) {
const ctx = document.getElementById(canvasId).getContext('2d');
new Chart(ctx, {
type: 'boxplot',
data: {
labels: [label],
datasets: [{ label: label, data: [data], backgroundColor: 'rgba(59, 130, 246, 0.5)', borderColor: '#3b82f6', borderWidth: 1 }]
},
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, title: { display: true, text: label } } }
});
}
function drawScatter(canvasId, xData, yData, xLabel, yLabel) {
const ctx = document.getElementById(canvasId).getContext('2d');
const points = xData.map((x, i) => ({ x: x, y: yData[i] }));
new Chart(ctx, {
type: 'scatter',
data: { datasets: [{ label: `${xLabel} vs Sales`, data: points, backgroundColor: 'rgba(16, 185, 129, 0.6)', pointRadius: 3 }] },
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, title: { display: true, text: `${xLabel} vs Sales` } }, scales: { x: { display: true, title: {display:false} }, y: { display: false } } }
});
}
function renderHeatmap(vars, rows, allHeaders) {
const table = document.getElementById('heatmap-table');
let html = '<thead><tr><th></th>';
vars.forEach(v => html += `<th>${v}</th>`);
html += '</tr></thead><tbody>';
vars.forEach((v1) => {
html += `<tr><th>${v1}</th>`;
vars.forEach((v2) => {
const val1 = rows.map(r => r[allHeaders.indexOf(v1)]);
const val2 = rows.map(r => r[allHeaders.indexOf(v2)]);
const corr = calculateCorr(val1, val2);
const color = corr > 0 ? `rgba(239, 68, 68, ${corr})` : `rgba(59, 130, 246, ${Math.abs(corr)})`;
const textColor = Math.abs(corr) > 0.5 ? 'white' : 'black';
html += `<td style="background:${color}; color:${textColor}">${corr.toFixed(2)}</td>`;
});
html += '</tr>';
});
html += '</tbody>';
table.innerHTML = html;
}
function calculateCorr(x, y) {
let n = x.length;
let sumX = x.reduce((a, b) => a + b, 0);
let sumY = y.reduce((a, b) => a + b, 0);
let sumXY = x.reduce((s, xi, i) => s + xi * y[i], 0);
let sumX2 = x.reduce((s, xi) => s + xi * xi, 0);
let sumY2 = y.reduce((s, yi) => s + yi * yi, 0);
let num = (n * sumXY) - (sumX * sumY);
let den = Math.sqrt((n * sumX2 - sumX*sumX) * (n * sumY2 - sumY*sumY));
return den === 0 ? 0 : num / den;
}
function callAI(mode, context, elementId) {
google.script.run.withSuccessHandler(text => {
document.getElementById(elementId).innerHTML = marked.parse(text);
}).askGeminiInsight(mode, context);
}
</script>
</body>
</html>