1) 데이터셋 선택 및 샘플링
2) Google Sheets에 업로드
3) 기본 EDA 수행 (학생 + Gemini 보조)
4) 분석 범위 확정 (상관관계/회귀/군집 중 선택)
5) Apps Script 대시보드 설계
6) Gemini API 연결
7) 분석 기능(EDA, 상관관계, 인사이트 생성) 구현
8) 대시보드 완성
9) 최종 마케팅 인사이트 보고서 도출
// ==========================================
// 🚀 통합 분석 센터 (Server: Code.gs) v4.0 - ERD, 상관관계
// ==========================================
// 1. 설정: Gemini API 키 (스크립트 속성에서 가져옴)
const GEMINI_API_KEY = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
const GEMINI_MODEL = 'gemini-2.5-flash';
// 2. 메뉴 생성 (시트 열릴 때 자동 실행)
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('🚀 통합 분석 센터')
.addItem('📊 대시보드 열기', 'showDashboard')
.addToUi();
}
// 3. HTML 대시보드 띄우기
function showDashboard() {
const html = HtmlService.createTemplateFromFile('Dashboard')
.evaluate()
.setTitle('📊 마케팅 데이터 종합 분석')
.setWidth(1200)
.setHeight(900);
SpreadsheetApp.getUi().showModalDialog(html, ' ');
}
// 4. [API] 데이터 전송 함수 (프론트엔드 요청 시 실행)
function getFullData() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const dataSheet = ss.getSheetByName('Data_Lake');
if (!dataSheet) throw new Error("'Data_Lake' 시트가 없습니다.");
const range = dataSheet.getDataRange();
const rawValues = range.getValues();
if (rawValues.length < 2) throw new Error("데이터가 부족합니다. (헤더 포함 2행 이상)");
const headers = rawValues[0];
const rows = rawValues.slice(1).filter(r => r[0] !== "");
// 기초 통계 계산
const stats = calculateBasicStats(headers, rows);
return {
headers: headers,
rows: rows,
stats: stats
};
}
// 5. 기초 통계 계산 로직
function calculateBasicStats(headers, rows) {
let stats = {};
headers.forEach((col, idx) => {
if (typeof rows[0][idx] === 'number') {
let vals = rows.map(r => r[idx]).filter(v => typeof v === 'number');
if (vals.length > 0) {
let mean = vals.reduce((a, b) => a + b, 0) / vals.length;
let sorted = [...vals].sort((a, b) => a - b);
let min = sorted[0];
let max = sorted[sorted.length - 1];
let median = sorted[Math.floor(sorted.length / 2)];
let variance = vals.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / vals.length;
let std = Math.sqrt(variance);
stats[col] = {
mean: mean.toFixed(2),
median: median,
min: min,
max: max,
std: std.toFixed(2),
count: vals.length
};
}
}
});
return stats;
}
// 6. AI 리포트 생성 함수
function askGeminiInsight(mode, contextData) {
if (!GEMINI_API_KEY) return "⚠️ API 키가 설정되지 않았습니다. [프로젝트 설정 > 스크립트 속성]에서 키를 추가하세요.";
const url = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;
// 데이터 길이 제한 (에러 방지)
const dataString = JSON.stringify(contextData).substring(0, 3000);
const prompt = `
데이터 분석가로서 아래 통계 데이터를 보고 핵심을 요약해주세요.
모드: ${mode}
데이터: ${dataString}
[작성 규칙]
1. 서론/결론 제외, 바로 본론 시작.
2. 다음 4가지 관점에서 각각 1~2줄로 요약:
- 분포 특성
- 이상치(Outlier) 존재 여부
- 변수 간 상관관계 핵심
- 판매량(Target) 영향 요인
3. 마크다운 형식으로 작성.
`;
try {
const payload = { contents: [{ parts: [{ text: prompt }] }] };
const options = { method: 'post', contentType: 'application/json', payload: JSON.stringify(payload), muteHttpExceptions: true };
const response = UrlFetchApp.fetch(url, options);
const json = JSON.parse(response.getContentText());
if (json.error) return "AI 오류: " + json.error.message;
return json.candidates[0].content.parts[0].text;
} catch (e) { return "AI 호출 실패: " + e.toString(); }
}<!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>첨부의 구글 시트 파일을 소스로 Gemini API를 통해 데이터 분석을 진행하는 Apps Script와 Dashboard를 만들려고 해.
일단 EDA 분석만 진행한다고 할때, 어떤 분석들을 진행하고 시각화할 수 있을까?
이 시각화 결과물들을 apps script를 통해서 깔끔하고 멋진 대시보드 html로 구현하고 싶어.
어떻게 구조화하면 될까?마우스를 올리면 숫자가 뜨는(Hover) 애니메이션 차트를 구현해주고,
반응형 레이아웃를 사용해서 창 크기에 따라 배치가 예쁘고 적절하게 바뀔 수 있게 해줘.
사용자 경험(UX)을 위해서 엑셀 시트를 스크롤할 필요 없이, 앱처럼 탭 버튼만 누르면 화면이 즉시 전환되면 좋겠어.
html은 팝업(modal) 방식으로 일단 구현하자.[ 📸 이미지와 함께 입력하는 메시지 예시 ]
[목표] 첨부한 이미지의 레이아웃과 디자인 스타일을 참고하여, 나의 Dashboard.html 코드를 작성해 주세요.
[디자인 요구사항 예시]
레이아웃: 이미지처럼 좌측 메뉴바 / 상단 카드 / 중앙 차트 배치를 잡아주세요. (이미지에 따라 변경)
색감: 이미지에 사용된 **메인 컬러(파란색 계열 등)**와 폰트 스타일을 비슷하게 적용해 주세요.
반응형: 화면 크기에 따라 배치가 자연스럽게 바뀌도록 해주세요.
[기능 제약 조건 (필수)] 단, 디자인은 이미지를 따르되 기능 구현은 아래 규칙을 지켜야 합니다.
차트는 이미지 그림 대신 Chart.js 라이브러리를 사용해서 구현해주세요.
html은 팝업(modal) 방식으로 구현해줘

구분 | 팝업 (Modal Dialog) | 웹앱 (Web App) |
장점 | "1. 데이터 연동 최적화: 현재 열린 시트 데이터를 즉시 가져올 수 있음. | "1. 전문성(UX): 독립된 URL이 생성되어 진짜 SaaS 소프트웨어처럼 보임 (전체 화면 활용). |
2. 개발 속도: 코드를 저장만 하면 바로 실행 가능 (배포 과정 없음). | 2. 모바일 지원: 스마트폰 브라우저에서도 깔끔하게 볼 수 있음. | |
3. 접근성: 시트 메뉴에서 바로 실행하므로 작업 흐름이 끊기지 않음." | 3. 독립성: 시트를 켜지 않고도 링크만으로 접속하여 데이터 확인 가능." | |
단점 | "1. 화면 제약: 구글 시트 위의 작은 창(Iframe)으로만 떠서 답답할 수 있음. | "1. 연동 복잡성: getActiveSpreadsheet() 사용 불가 (특정 시트 ID를 코드에 고정해야 함). |
2. 휘발성: 창을 닫거나 새로고침하면 모든 상태가 초기화됨. | 2. 배포 번거로움: 코드를 수정할 때마다 **[새 버전 배포]**를 눌러야 사용자에게 반영됨. | |
3. 모바일 불가: 모바일 구글 시트 앱에서는 작동하지 않음." | 3. 권한 설정: 사용자 접근 권한 처리가 조금 더 까다로울 수 있음." |
**[당신의 역할]**
당신은 데이터 시각화와 웹 개발 능력을 갖춘 **'스마트 연구 보조원'**입니다.
제가 가진 데이터를 분석해서, 연구 논문에 활용할 수 있는 수준의 **'웹 대시보드'**를 만들어 주세요.
**1. [내가 원하는 도구: 무엇을 만드나요?]**
구글 시트에 있는 데이터를 가져와서, 웹 브라우저 팝업창에서 바로 볼 수 있는 **'마케팅 분석 리포트 시스템'**을 구축해 주세요.
`Code.gs`(서버)와 `Dashboard.html`(화면) 두 개의 코드를 작성해 주면 됩니다.
* **기능 1 (화면 구성):**
* 상단에는 `[📋 기초 통계 분석]`, `[📘 분석 가이드]` 두 개의 탭을 만들어 주세요.
* 데이터를 불러오는 동안에는 "분석 중입니다..."라는 로딩 표시를 띄워주세요.
* **기능 2 (기초 통계 탭 - 핵심!):**
* 화면 맨 위에는 **'AI 연구 요약'** 박스를 만들어서, 데이터의 핵심 내용을 글로 정리해 주세요.
* 그 아래에는 **4가지 핵심 분석 차트**를 바둑판 모양(그리드)으로 배치해 주세요.
* ① 데이터 분포 (히스토그램)
* ② 이상치 탐색 (박스 플롯)
* ③ 상관관계 분석 (산점도)
* ④ 상관계수 표 (히트맵)
* **중요:** 논문에 바로 쓸 수 있도록, 각 차트 옆에 **[📷 이미지 저장]** 버튼을 꼭 만들어 주세요.
* **기능 3 (분석 가이드 탭):**
* 통계가 낯선 사람들을 위해 '히스토그램', '박스 플롯' 등이 무엇인지 설명해 주는 도움말 페이지를 만들어 주세요.
**2. [데이터 설명: 어떤 데이터를 쓰나요?]**
* 데이터는 `Data_Lake`라는 시트에 있습니다.
* 첫 번째 줄은 제목(변수명)이고, 두 번째 줄부터 실제 데이터입니다.
* **변수 정보:**
* `TV`, `Billboards`, `Google_Ads` 등 광고비 변수 (숫자)
* `Product_Sold` (판매량, 숫자)
* `Group` (실험군/대조군 구분, 문자 A/B)
**3. [제작 규칙: 이렇게 만들어 주세요]**
* html은 팝업(modal) 방식으로 구현해 주세요.
* 차트는 예쁘고 반응이 빠른 `Chart.js` 라이브러리를 사용해 주세요.
* 디자인은 깔끔하고 현대적인 스타일(CSS Flexbox 활용)로 해주세요.
* AI 기능을 쓸 때 필요한 'API 키'는 코드에 직접 적지 말고, 안전하게 설정(PropertiesService)에서 불러오도록 해주세요.
* 만약 데이터가 없거나 오류가 나면, 그냥 멈추지 말고 "데이터를 확인해 주세요"라고 친절하게 알려주세요.
