라이언 Dashboard 만들어보기 - 회귀분석 추가

1. Regression 추가 따라하기

Google DocsAdvertising_Data > 사본 만들고 > 각자 드라이브에 저장
데이터 시트를 Data_Lake로 변경 (대소문자 정확히)
아래 두 코드로 Apps Script 생성
Code.gs 복사 & 붙여넣기
Code.gs - 펼쳐보기
// ========================================== // 🚀 통합 분석 센터 (Server: Code.gs) v4.0 - 회귀분석 추가 // ========================================== // 1. 설정: Gemini API 키 const GEMINI_API_KEY = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY'); const GEMINI_MODEL = 'gemini-2.5-pro'; // 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("데이터가 부족합니다."); 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 variance = vals.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / vals.length; stats[col] = { mean: mean.toFixed(2), median: sorted[Math.floor(sorted.length / 2)], min: sorted[0], max: sorted[sorted.length - 1], std: Math.sqrt(variance).toFixed(2), count: vals.length }; } } }); return stats; } // 6. AI 리포트 생성 함수 (EDA용) function askGeminiInsight(mode, contextData) { const prompt = ` 데이터 분석가로서 아래 통계 데이터를 보고 핵심을 요약해주세요. 모드: ${mode} 데이터: ${JSON.stringify(contextData).substring(0, 3000)} [작성 규칙] 1. 서론/결론 제외, 바로 본론 시작. 2. 다음 4가지 관점에서 각각 1~2줄로 요약: - 분포 특성, 이상치, 상관관계, 판매량 영향 요인 3. 마크다운 형식으로 작성. `; return callGeminiAPI(prompt, true); } // ========================================== // 🚀 [NEW] 회귀분석 및 시나리오 핸들러 (최종 수정됨) // ========================================== // 7-1. [버튼 1] 회귀분석 실행 (계수 + 리포트 + 시트 저장) function runRegressionFromDashboard() { const csvString = getCsvData(); const prompt = ` 당신은 데이터 과학자입니다. 아래 마케팅 데이터를 바탕으로 'Product_Sold'를 종속변수로 하는 다중회귀분석을 수행하세요. [데이터] ${csvString} [요청사항] 1. 각 채널(변수)의 회귀계수(Coefficient)와 절편(Intercept)을 추정하세요. 2. 회귀분석 결과에 대한 비즈니스 인사이트 리포트를 작성하세요. 3. 결과는 반드시 아래 JSON 포맷으로 출력하세요. { "coefficients": { "TV": 0.1, "Social_Media": 0.5, "Intercept": 10.0 }, "report": "여기에 마크다운 형식으로 분석 결과를 서술하세요." } `; const result = callGeminiAPI(prompt, false); // model_summary 시트에 결과 저장 saveRegressionToSheet(result); return result; } // 7-2. [버튼 2] 시나리오 제안 (제약조건 반영 + 시트 저장) function runScenarioFromDashboard(totalBudget, channels, minShares, maxShares) { const csvString = getCsvData(); let constraintsInfo = ""; for(let i=0; i<channels.length; i++) { constraintsInfo += `- ${channels[i]}: 최소 ${(minShares[i]*100).toFixed(0)}% ~ 최대 ${(maxShares[i]*100).toFixed(0)}%\n`; } const prompt = ` 당신은 CMO(최고 마케팅 책임자)입니다. 데이터를 분석하여 총 예산 $${totalBudget}에 대한 최적의 배분 시나리오 3가지를 제안하세요. [데이터] ${csvString} [제약 조건 (반드시 준수)] ${constraintsInfo} [요청사항] 1. 위 제약 조건을 지키면서 3가지 전략(ROI 극대화, 리스크 분산, 실험적)을 수립하세요. 2. 각 시나리오별 예상 판매량(expected_sales)을 데이터에 기반해 추산하세요. 3. 결과는 반드시 아래 JSON 포맷으로 출력하세요. { "scenarios": [ { "name": "1. ROI 극대화형", "description": "전략 설명...", "allocation": { "TV": 1000, "Social_Media": 5000, ... }, "expected_sales": 15000 } ] } `; const result = callGeminiAPI(prompt, false); // 프론트엔드가 'predictedSales'를 기대하므로 필드명 매핑 if (result.scenarios) { result.scenarios.forEach(s => { // expected_sales가 있으면 predictedSales로 복사 (대소문자 무관하게 처리) if (s.expected_sales !== undefined) s.predictedSales = s.expected_sales; if (s.PredictedSales !== undefined) s.predictedSales = s.PredictedSales; }); } // optimizer_ui 시트에 결과 저장 saveScenariosToSheet(result, totalBudget); return result; } // [Helper] 데이터 CSV 변환 (콤마 처리 강화) function getCsvData() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const dataSheet = ss.getSheetByName('Data_Lake'); if (!dataSheet) throw new Error("'Data_Lake' 시트가 없습니다."); const rawValues = dataSheet.getDataRange().getValues(); if (rawValues.length < 10) throw new Error("데이터가 너무 적습니다."); // 콤마가 들어간 데이터가 있을 경우를 대비해 안전하게 처리 return rawValues.map(row => row.map(cell => (cell === null ? "" : cell.toString().replace(/,/g, ""))).join(",") ).join("\n"); } // [Helper] Gemini API 호출 통합 함수 (JSON 파싱 강화) function callGeminiAPI(prompt, isTextMode) { if (!GEMINI_API_KEY) throw new Error("API 키가 설정되지 않았습니다."); const url = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`; const options = { method: 'post', contentType: 'application/json', payload: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }), muteHttpExceptions: true }; try { const response = UrlFetchApp.fetch(url, options); const json = JSON.parse(response.getContentText()); if (json.error) throw new Error(json.error.message); let content = json.candidates[0].content.parts[0].text; // 텍스트 모드면 바로 반환 if (isTextMode) return content; // JSON 파싱 강화 (앞뒤 잡담 제거) // 1. 마크다운 제거 content = content.replace(/```json/g, "").replace(/```/g, "").trim(); // 2. { 로 시작해서 } 로 끝나는 부분만 추출 const firstBrace = content.indexOf("{"); const lastBrace = content.lastIndexOf("}"); if (firstBrace !== -1 && lastBrace !== -1) { content = content.substring(firstBrace, lastBrace + 1); } return JSON.parse(content); } catch (e) { throw new Error("AI 호출 실패: " + e.message); } } // [Helper] 회귀분석 결과 시트 저장 function saveRegressionToSheet(result) { const ss = SpreadsheetApp.getActiveSpreadsheet(); let sheet = ss.getSheetByName('model_summary'); if (!sheet) { sheet = ss.insertSheet('model_summary'); } else { sheet.clear(); } // 헤더 작성 sheet.getRange("A1:C1").setValues([["변수명", "계수", "분석 리포트"]]); sheet.getRange("A1:C1").setBackground("#EFEFEF").setFontWeight("bold"); // 데이터 작성 let rows = []; const coeffs = result.coefficients; for (let key in coeffs) { rows.push([key, coeffs[key], ""]); } if (rows.length > 0) { sheet.getRange(2, 1, rows.length, 3).setValues(rows); // 리포트는 첫 번째 행의 C열에 넣고 병합 sheet.getRange(2, 3).setValue(result.report); sheet.getRange(2, 3, rows.length, 1).merge().setVerticalAlignment("top").setWrapStrategy(SpreadsheetApp.WrapStrategy.WRAP); } } // [Helper] 시나리오 결과 시트 저장 function saveScenariosToSheet(result, totalBudget) { const ss = SpreadsheetApp.getActiveSpreadsheet(); let sheet = ss.getSheetByName('optimizer_ui'); if (!sheet) { sheet = ss.insertSheet('optimizer_ui'); } else { sheet.clear(); } sheet.getRange("A1").setValue(`총 예산: $${totalBudget}`); // 데이터 준비 const scenarios = result.scenarios; if (!scenarios || scenarios.length === 0) return; // 헤더 만들기 const channelKeys = Object.keys(scenarios[0].allocation); const headers = ["시나리오명", "설명", "예상 판매량", ...channelKeys]; let startRow = 3; sheet.getRange(startRow, 1, 1, headers.length).setValues([headers]).setBackground("#EFEFEF").setFontWeight("bold"); let outputData = []; scenarios.forEach(s => { let row = [ s.name, s.description, s.predictedSales || s.expected_sales || 0 // 필드명 방어 ]; channelKeys.forEach(ch => { row.push(s.allocation[ch] || 0); }); outputData.push(row); }); if (outputData.length > 0) { sheet.getRange(startRow + 1, 1, outputData.length, headers.length).setValues(outputData); } }
Dashboard.html 생성 > 복사 & 붙여넣기
Dashboard.html - 펼쳐보기
<!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('reg')">💰 회귀분석(버짓제안)</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> <div id="view-reg" class="view-section"> <!-- 회귀 분석 카드 --> <div class="card"> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;"> <div class="section-title" style="margin:0; border-left:none; padding-left:0;">1. 회귀분석 결과</div> <button class="tab-btn" style="flex:none; padding:8px 12px;" onclick="runRegression()">📈 회귀분석 실행</button> </div> <div id="reg-report" class="ai-box" style="margin-top:10px;"> <div class="ai-header">🤖 회귀 기반 인사이트 리포트</div> <div id="reg-report-body">"회귀분석 실행" 버튼을 클릭</div> </div> <div class="section-title" style="margin-top:20px;">계수 테이블</div> <div id="reg-coeff-table"></div> </div> <!-- 산점도 + 회귀선 --> <div class="section-title">2. 채널별 판매량 관계 (산점도 + 회귀선)</div> <div class="chart-grid" id="reg-scatter-grid"></div> <!-- 시나리오 카드 --> <div class="card" style="margin-top:20px;"> <div class="section-title" style="margin:0; border-left:none; padding-left:0;">3. 예산 시나리오 (3가지 전략)</div> <div style="margin-top:10px;"> <label>총 예산 ($): </label> <input id="scenario-total-budget" type="number" style="width:120px; padding:4px; margin-right:20px;" /> </div> <div style="margin-top:10px; overflow-x:auto;"> <table class="heatmap-table" style="font-size:0.8rem; min-width:600px;"> <thead> <tr> <th>채널</th> <th>최소 비중 (0~1)</th> <th>최대 비중 (0~1)</th> </tr> </thead> <tbody> <!-- 고정 6개 채널 --> <tr> <td>TV</td> <td><input id="min-TV" type="number" step="0.01" style="width:80px;" value="0.05"></td> <td><input id="max-TV" type="number" step="0.01" style="width:80px;" value="0.5"></td> </tr> <tr> <td>Billboards</td> <td><input id="min-Billboards" type="number" step="0.01" style="width:80px;" value="0.05"></td> <td><input id="max-Billboards" type="number" step="0.01" style="width:80px;" value="0.5"></td> </tr> <tr> <td>Google_Ads</td> <td><input id="min-Google_Ads" type="number" step="0.01" style="width:80px;" value="0.05"></td> <td><input id="max-Google_Ads" type="number" step="0.01" style="width:80px;" value="0.5"></td> </tr> <tr> <td>Social_Media</td> <td><input id="min-Social_Media" type="number" step="0.01" style="width:80px;" value="0.05"></td> <td><input id="max-Social_Media" type="number" step="0.01" style="width:80px;" value="0.5"></td> </tr> <tr> <td>Influencer_Marketing</td> <td><input id="min-Influencer_Marketing" type="number" step="0.01" style="width:80px;" value="0.05"></td> <td><input id="max-Influencer_Marketing" type="number" step="0.01" style="width:80px;" value="0.5"></td> </tr> <tr> <td>Affiliate_Marketing</td> <td><input id="min-Affiliate_Marketing" type="number" step="0.01" style="width:80px;" value="0.05"></td> <td><input id="max-Affiliate_Marketing" type="number" step="0.01" style="width:80px;" value="0.5"></td> </tr> </tbody> </table> </div> <div style="margin-top:10px; text-align:right;"> <button class="tab-btn" style="flex:none; padding:8px 12px;" onclick="runScenarios()">💡 3가지 전략 시나리오 제안</button> </div> <div id="scenario-result" style="margin-top:15px;"></div> </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); } let regressionResult = null; function runRegression() { if (!globalData) { alert("데이터가 아직 로드되지 않았습니다."); return; } document.getElementById('reg-report-body').innerText = "Gemini가 회귀분석을 수행 중입니다..."; google.script.run .withSuccessHandler(showRegressionResult) .withFailureHandler(showError) .runRegressionFromDashboard(); } function showRegressionResult(result) { regressionResult = result; const report = result.report || "(리포트 없음)"; const coeffs = result.coefficients || {}; // 리포트 (마크다운 → HTML) document.getElementById('reg-report-body').innerHTML = marked.parse(report); // 계수 테이블 const tableDiv = document.getElementById('reg-coeff-table'); const order = ["TV", "Billboards", "Google_Ads", "Social_Media", "Influencer_Marketing", "Affiliate_Marketing", "Intercept"]; let html = '<table class="heatmap-table"><thead><tr><th>변수명</th><th>계수</th></tr></thead><tbody>'; order.forEach(k => { if (coeffs.hasOwnProperty(k)) { html += `<tr><td>${k}</td><td>${coeffs[k]}</td></tr>`; } }); html += '</tbody></table>'; tableDiv.innerHTML = html; // 산점도 + 회귀선 렌더 renderRegressionScatters(); } function computeSimpleRegression(x, y) { const n = x.length; if (!n) return { slope: 0, intercept: 0 }; let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0; for (let i = 0; i < n; i++) { sumX += x[i]; sumY += y[i]; sumXY += x[i] * y[i]; sumX2 += x[i] * x[i]; } const denom = (n * sumX2 - sumX * sumX); if (denom === 0) return { slope: 0, intercept: sumY / n }; const slope = (n * sumXY - sumX * sumY) / denom; const intercept = (sumY - slope * sumX) / n; return { slope, intercept }; } function renderRegressionScatters() { const targetVar = 'Product_Sold'; const headers = globalData.headers; const rows = globalData.rows; const scatterGrid = document.getElementById('reg-scatter-grid'); scatterGrid.innerHTML = ''; const channelNames = ['TV', 'Billboards', 'Google_Ads', 'Social_Media', 'Influencer_Marketing', 'Affiliate_Marketing']; channelNames.forEach(col => { const xIdx = headers.indexOf(col); const yIdx = headers.indexOf(targetVar); if (xIdx === -1 || yIdx === -1) return; const xVals = rows.map(r => Number(r[xIdx])).filter(v => !isNaN(v)); const yValsRaw = rows.map(r => Number(r[yIdx])); const yVals = yValsRaw.filter((v, i) => !isNaN(xVals[i]) && !isNaN(v)); if (!xVals.length || !yVals.length) return; const div = createChartCard(`regscat-${col}`); scatterGrid.appendChild(div); const points = []; for (let i = 0; i < xVals.length; i++) { points.push({ x: xVals[i], y: yVals[i] }); } const { slope, intercept } = computeSimpleRegression(xVals, yVals); const minX = Math.min(...xVals); const maxX = Math.max(...xVals); const linePoints = [ { x: minX, y: slope * minX + intercept }, { x: maxX, y: slope * maxX + intercept } ]; const ctx = document.getElementById(`regscat-${col}`).getContext('2d'); new Chart(ctx, { type: 'scatter', data: { datasets: [ { type: 'scatter', label: `${col} vs Product_Sold`, data: points, backgroundColor: 'rgba(16, 185, 129, 0.6)', pointRadius: 3 }, { type: 'line', label: '회귀선', data: linePoints, borderColor: 'rgba(37, 99, 235, 0.9)', borderWidth: 2, fill: false, pointRadius: 0 } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, title: { display: true, text: `${col} vs Product_Sold (회귀선 포함)` } }, scales: { x: { display: true, title: { display: true, text: col } }, y: { display: true, title: { display: true, text: 'Product_Sold' } } } } }); }); } function runScenarios() { const totalBudget = Number(document.getElementById('scenario-total-budget').value || 0); if (!totalBudget || totalBudget <= 0) { alert("총 예산을 입력해 주세요."); return; } const channels = ['TV', 'Billboards', 'Google_Ads', 'Social_Media', 'Influencer_Marketing', 'Affiliate_Marketing']; const minShares = []; const maxShares = []; channels.forEach(ch => { const minVal = Number(document.getElementById('min-' + ch).value || 0); const maxVal = Number(document.getElementById('max-' + ch).value || 1); minShares.push(minVal); maxShares.push(maxVal); }); const resultDiv = document.getElementById('scenario-result'); resultDiv.innerHTML = "Gemini가 3가지 전략 시나리오를 생성 중입니다..."; google.script.run .withSuccessHandler(showScenarioResult) .withFailureHandler(showError) .runScenarioFromDashboard(totalBudget, channels, minShares, maxShares); } function showScenarioResult(result) { const scenarios = result.scenarios || []; if (!scenarios.length) { document.getElementById('scenario-result').innerHTML = "시나리오 결과가 없습니다."; return; } let html = ''; scenarios.forEach(s => { html += ` <div class="ai-box" style="margin-bottom:10px;"> <div class="ai-header">${s.name || '시나리오'}</div> <div style="margin-bottom:8px;">${(s.description || '').replace(/\n/g, '<br>')}</div> <div><strong>📈 예상 판매량:</strong> ${s.predictedSales || 0}</div> <div style="margin-top:8px;"> <strong>채널별 예산 배분:</strong> <ul style="margin:4px 0 0 18px; padding:0;"> `; const alloc = s.allocation || {}; Object.keys(alloc).forEach(ch => { html += `<li>${ch}: $${Math.round(alloc[ch])}</li>`; }); html += ` </ul> </div> </div> `; }); document.getElementById('scenario-result').innerHTML = html; } function showError(err) { console.error(err); alert("오류가 발생했습니다: " + err); const regReportEl = document.getElementById('reg-report-body'); const scenarioResultEl = document.getElementById('scenario-result'); if (regReportEl) regReportEl.innerText = "회귀분석 중 오류가 발생했습니다."; if (scenarioResultEl) scenarioResultEl.innerText = "시나리오 생성 중 오류가 발생했습니다."; } </script> </body> </html>
추가된 회귀분석(버짓제안) 확인
회귀분석 실행 클릭
회귀분석 결과 확인
예산 시나리오 (3가지 전략) > 총 예산 ($) : 10,000 입력 > 3가지 전략 시나리오 제안 클릭
회귀분석 결과 내용이 model_summary 시트에 저장된 거 확인
예산 시나리오 (3가지 전략)optimizer_ui 시트에 저장된 거 확인

참조용. 라이언 대시보드 : 회귀분석 추가 직접 해보고 싶은 사람을 위한 가이드 (v3에서 v4로)

1단계 — v3 코드 구조 이해 요청

아래 v3 코드를 읽고, - 어떤 기능을 하는지 - 데이터가 어떻게 흐르는지 - 어떤 화면(UI)과 분석이 구현되어 있는지 를 비전공자도 이해할 수 있게 간단히 설명해 주세요. (이 단계에서는 수정 없이 “설명”만 합니다.) (여기에 v3 Code.gs) (여기에 v3 Dashboard.html) 혹은 첨부

✅ 2단계 — v4 기능을 위한 전체 설계도 작성

v3 팝업 대시보드를 아래 기능을 포함한 v4로 확장하기 위한 “전체 시스템 설계도”를 작성해 주세요. 필요한 기능: - 회귀분석(요약문 + 계수표 + 채널별 산점도+회귀선) - 예산 시나리오 3종 생성(전략 설명 + 예상 판매량) - AI 인사이트 확장 - 기존 EDA 탭 유지, 새로운 탭 추가 설계도에는 다음을 포함해 주세요: - 어떤 탭이 존재하며 각각 어떤 UI 요소가 필요한지 - 백엔드(GAS)와 프론트엔드(HTML/JS)가 어떻게 연결되는지 - 어떤 계산이 어디에서 수행되는지 - 자동 생성 시트(model_summary, optimizer_ui)의 역할과 데이터 흐름

✅ 3단계 — v4 Code.gs 전체 구현 (자동 생성 시트 포함)

v3 Code.gs를 기반으로 아래 기능을 모두 포함하는 “완성된 v4 Code.gs 전체 파일”을 작성해 주세요. 반드시 포함해야 하는 기능: 1) 회귀분석 계산 및 결과 반환 2) 총예산 입력 → 3가지 예산 전략 생성 및 예상 판매량 계산 3) AI 인사이트 생성 4) 기존 getFullData() 및 v3 기능 유지 5) **model_summary 시트 자동 생성** 6) **optimizer_ui 시트 자동 생성** 시트 자동 생성 요건: - 시트가 없으면 insertSheet()로 생성 - 필요한 헤더/기본 구조 자동 세팅 - 회귀분석 또는 시나리오 계산 시 활용 가능하도록 구성 요청: - 비전공자도 이해하기 쉬운 주석 포함 - 그대로 붙여넣어 바로 실행 가능한 완성본이어야 함 출력 형식: [BEGIN FINAL CODE.GS] (여기에 완성된 v4 Code.gs 전체) [END FINAL CODE.GS]

4단계 — Dashboard.html 생성 요청

v3 Dashboard.html을 기반으로, v4 기능을 모두 반영한 “완성된 Dashboard.html 전체 파일”을 작성해 주세요. 필수 구성: - 기존 EDA 탭 유지 - 회귀분석 탭 추가 - 분석 실행 버튼 - 요약문 표시 - 계수표 표시 - 채널별 산점도+회귀선(Chart.js) - 예산 시나리오 탭 추가 - 총 예산 입력 - 최소·최대 비중 입력 - 전략 3종 생성 버튼 - 전략 카드(전략명·설명·예상 판매량·배분표) - marked.js, Chart.js 포함 - 백엔드 호출 기능 정상 연결 요청: - 비전공자용 주석 포함 - 붙여넣으면 바로 동작하는 완성형 HTML/JS 파일 출력 형식: [BEGIN FINAL DASHBOARD.HTML] (여기에 완성된 v4 Dashboard.html 전체) [END FINAL DASHBOARD.HTML]

5단계 — 최종 통합·호환성 점검

위에서 생성한 v4 Code.gs / Dashboard.html이 - 팝업에서 정상적으로 뜨는지 - 탭 전환이 모두 동작하는지 - 회귀/시나리오/EDA 모든 기능이 충돌 없이 실행되는지 - 자동 생성 시트(model_summary / optimizer_ui)가 정확히 동작하는지 - 백엔드와 프론트엔드 함수 연결이 일치하는지 를 점검해 주세요. 필요하면 코드 일부를 자동으로 수정하여 “서로 완벽히 호환되는 최종 버전”으로 정리해 주세요. 출력 형식: [BEGIN FINAL CODE.GS] (최종 정리된 Code.gs) [END FINAL CODE.GS] [BEGIN FINAL DASHBOARD.HTML] (최종 정리된 Dashboard.html) [END FINAL DASHBOARD.HTML]

6단계 — 최종 사용 가이드 생성 요청

비전공자용 “간단 사용 가이드”를 작성해 주세요. 필수 포함: - Code.gs / Dashboard.html을 어디에 붙여넣는지 - 구글 시트에서 팝업 대시보드를 띄우는 방법 - 원본 데이터 시트 구성(광고비 + Product_Sold) - 회귀분석 탭 사용법 - 예산 시나리오 탭 사용법 - **model_summary / optimizer_ui 시트는 자동 생성됨** 안내 길게 쓸 필요는 없고, “무엇을 어디서 누르면 되는지”를 간단히 정리해 주세요.

통합 프롬프트

통합 프롬프트 펼쳐보기
당신은 Google Apps Script(GAS) + HTML/JS로 “구글 시트 팝업(modal) 기반 대시보드”를 설계·구현하는 시니어 엔지니어입니다. 저는 개발 비전공자인 광고/마케팅 실무자이고, 지금 가진 것은 v3 버전의 Code.gs / Dashboard.html 코드뿐입니다. v4라는 코드는 아직 존재하지 않는다고 가정해 주세요. ====================================================================== 🎯 목표 ====================================================================== v3 팝업 대시보드를 기반으로, 아래 기능을 모두 포함하는 “v4 팝업 대시보드”를 **설계 + 구현 + 통합 + 간단 사용법 안내**까지 한 번에 완성해 주세요. - v3와 동일하게 **웹앱이 아니라 구글 시트 메뉴에서 띄우는 팝업(modal) UI** 형태로 동작해야 합니다. - 제가 할 일은 최종적으로 “Code.gs 전체”, “Dashboard.html 전체” 두 파일을 복사해서 붙여넣는 것뿐이어야 합니다. ====================================================================== 📌 전제: v4에서 필요한 핵심 기능 (1~5단계 역할) ====================================================================== [1] v3 코드 구조 이해(간단 설명) - v3 Code.gs / Dashboard.html이 현재 어떤 기능을 하는지 - 데이터가 어디서 어떻게 읽혀서 팝업에 전달되는지 - 팝업 화면(탭, 차트 등)이 어떤 구조로 그려지는지 이 세 가지를, **비전공자인 제가 이해할 수 있을 정도의 수준으로 짧게 요약해 주세요. (이 단계에서는 코드를 수정하지 말고 “설명만” 해 주세요.)** [2] v4 기능 요구사항에 맞는 전체 설계도 v3를 확장해서 v4를 만들기 위해, 아래 기능이 **어떻게 추가·연결**되어야 하는지 설계해 주세요. 2-1. 회귀분석 탭 - 광고비 → 판매량(Product_Sold) 관계를 분석하는 회귀분석 기능 - 분석 결과를 자연어 문장으로 요약 - 광고 채널별 산점도 + 추세선(회귀선) - 변수(채널)별 중요도/계수 요약 표 - 이 모든 것을 볼 수 있는 **전용 탭(뷰)** 2-2. 예산 시나리오(버짓 제안) 탭 - “총 광고 예산”을 입력하면 - 예: 안정형 / 균형형 / 공격형 같은 **3가지 예산 전략** 자동 생성 - 각 전략마다 예상 판매량(Product_Sold 예측값)을 계산 - 각 전략별 채널 예산 배분 결과를 카드 형태/리스트 형태로 보여줌 - 이것을 위한 **전용 탭(뷰)** 2-3. AI 인사이트 확장 - 기존 v3의 “기초 통계 요약”뿐 아니라 - 회귀분석 결과에 대한 설명 - 예산 전략 3가지에 대한 설명 도 함께 자연어로 요약해 주는 인사이트 영역이 포함되어야 합니다. 2-4. UI 구조 - 기존 “기초 통계(EDA)” 탭은 유지 - 여기에 “회귀분석(버짓제안)” 탭 + “분석 가이드(Help)” 탭이 존재 - 전부 **구글 시트 팝업** 안에서 탭 전환으로 사용 - v3 스타일(Inter 폰트, 카드, 그리드, 색감)을 유지하면서 기능만 확장 이 단계에서는 **어떤 탭에 어떤 요소(버튼, 표, 차트, 텍스트)가 있어야 하는지**, **백엔드와 프론트엔드가 어떤 식으로 데이터를 주고받을지**, **어떤 계산이 어디에서 수행되는지**를 구조적으로 설명해 주세요. [3] v4 백엔드 Code.gs 전체 구현 이제 v3 Code.gs를 기반으로, v4 기능을 수행하는 **완성된 Code.gs 전체 파일**을 작성해 주세요. 필수 조건: - 기존 v3 기능(데이터 로딩, 기본 EDA 호출 등)은 유지 - 추가로 아래 기능을 포함해야 합니다. 3-1. 회귀분석 관련 - 스프레드시트에 있는 원본 데이터(광고비 + Product_Sold)를 이용해 회귀분석 수행 - 회귀 계수, 절편, 요약 통계 등을 계산해 반환 - 프론트엔드에서 사용할 수 있도록 적절한 JSON 구조로 반환 - Gemini(또는 유사 모델)를 사용해 “회귀 결과 설명” 텍스트 생성 3-2. 예산 시나리오 관련 - 입력받은 총 예산, 채널 목록, 채널별 최소/최대 비중 정보를 사용 - 3가지 서로 다른 전략(예: 안정형 / 균형형 / 공격형)의 예산 배분안을 계산 - 각 전략별 예산 배분 + 예상 판매량을 계산해 JSON 구조로 반환 - 필요하다면 AI를 사용해 각 전략에 대한 설명 문장을 생성 3-3. 자동 시트 생성 (중요) 아래 두 시트는 **사용자가 수동 생성할 필요 없이, Code.gs가 자동으로 생성**해야 합니다. - model_summary 시트 - 회귀분석 결과(계수, 요약 텍스트 등)를 저장/갱신하는 시트 - 존재 여부를 확인하고, 없으면 생성 - 기본 헤더/구조를 초기화하는 로직 포함 - optimizer_ui 시트 - 예산 시나리오용 설정/중간 값을 저장하는 시트 - 존재 여부를 확인하고, 없으면 생성 - 기본 헤더/구조를 초기화하는 로직 포함 즉, Code.gs에는 다음 패턴이 반드시 포함되어야 합니다: - 시트 존재 여부 확인 → 없으면 insertSheet로 생성 → 헤더/기본 구조 작성 3-4. AI 인사이트 함수 - 프론트엔드에서 호출하는 하나의 함수(예: askGeminiInsight 등)를 통해 - 모드/컨텍스트에 따라 다른 유형의 요약 문장을 생성 - JSON 또는 문자열 형태로 반환 3-5. 팝업 오픈 및 메뉴 설정 - 스프레드시트에서 메뉴를 추가해 팝업을 띄우는 onOpen / showSidebar or showModalDialog 관련 로직 - v3에서 사용하던 팝업 호출 방식을 유지·확장 요청: - 최종 결과는 제가 그대로 붙여넣어 사용할 수 있도록 **전체 Code.gs 파일**로 제공해 주세요. - 비전공자를 위해 각 기능 블록에 한글 주석을 달아 주세요. - 아래 형식으로 출력해 주세요: [BEGIN FINAL CODE.GS] (여기에 완성된 v4 Code.gs 전체 코드) [END FINAL CODE.GS] [4] v4 프론트엔드 Dashboard.html 전체 구현 이번에는 v3 Dashboard.html을 기반으로, v4 기능을 모두 반영한 **완성된 HTML/JS 파일**을 작성해 주세요. 필수 조건: - v3와 동일하게 팝업(modal)에 렌더링되는 HTML이어야 합니다. - 기존 기초 통계(EDA) 탭은 그대로 유지합니다. - 추가 탭: - 💰 회귀분석(버짓제안) 탭 - 회귀분석 실행 버튼 - 회귀 리포트 영역(마크다운 → HTML 렌더) - 회귀 계수 테이블 - 채널별 산점도 + 회귀선 Chart.js 시각화 - 📘 분석 가이드(Help) 탭 - 그래프/통계 해석 도움말 - 예산 시나리오 UI: - 총 예산 입력 필드 - 6개 채널(TV, Billboards, Google_Ads, Social_Media, Influencer_Marketing, Affiliate_Marketing)의 최소/최대 비중 입력 필드 - “3가지 전략 생성” 버튼 - 결과 카드(전략명, 설명, 예상 판매량, 채널별 예산 배분 리스트) - 백엔드 호출: - 회귀분석 실행 버튼 → 백엔드 회귀 분석 함수 호출 → 결과를 화면에 표시 - 3가지 전략 생성 버튼 → 백엔드 시나리오 생성 함수 호출 → 결과를 화면에 표시 - 기초 통계 EDA 영역은 기존 v3 동작(로드 시 or 버튼 클릭 시 getFullData + 차트 렌더) 유지 - Chart.js, boxplot 플러그인, marked.js를 계속 사용 요청: - 최종 결과는 제가 그대로 붙여넣어 사용할 수 있도록 **전체 Dashboard.html 파일**로 제공해 주세요. - 비전공자를 위해 주요 영역과 스크립트에 한글 주석을 달아 주세요. - 아래 형식으로 출력해 주세요: [BEGIN FINAL DASHBOARD.HTML] (여기에 완성된 v4 Dashboard.html 전체 코드) [END FINAL DASHBOARD.HTML] [5] 최종 통합·호환성 점검 위에서 생성한 Code.gs / Dashboard.html이 서로 잘 맞물려 실제로 동작하도록, 다음을 점검해 주세요. - 팝업 UI가 정상적으로 뜨는지 - 탭 전환이 정상적으로 동작하는지 - v3의 EDA 탭과 새로 추가된 회귀/시나리오 탭이 함께 문제 없이 작동할 수 있는 구조인지 - 백엔드 함수명, 인자 구조, 반환 JSON 구조가 프론트엔드 호출과 일치하는지 - model_summary / optimizer_ui 자동 생성 로직이 실제로 호출되도록 연결되어 있는지 필요하다면 Code.gs와 Dashboard.html 일부를 수정해도 좋으니, 반드시 “서로 호환되는 최종 버전”으로 정리한 뒤 위의 형식으로 제공해 주세요. ====================================================================== 📌 6단계: 최종 사용 가이드(간단 버전만) ====================================================================== 기능 설명은 위에서 충분히 했으므로, 최종 사용 가이드는 **아주 간단히**만 작성해 주세요. - Code.gs / Dashboard.html 파일을 어디에 붙여넣는지 (어떤 프로젝트/파일에 어떤 식으로 교체하면 되는지) - 구글 시트에서 메뉴를 눌러 팝업 대시보드를 띄우는 방법 - 원본 데이터 시트(광고비 + Product_Sold)의 기본 구조만 간단히 설명 - 회귀분석 탭 / 예산 시나리오 탭을 어떻게 사용하는지 한두 문장씩 - model_summary / optimizer_ui 시트는 사용자가 만들 필요 없고, 자동 생성된다는 점만 명확히 안내 이 6단계 사용 가이드는 길게 작성할 필요 없고, 실무자가 “어디서 뭐 누르면 된다” 정도만 알 수 있게 간단히 작성해 주세요. ====================================================================== 📌 입력(v3 원본 코드) ====================================================================== 첨부의 v3 코드를 분석해서, 위 요구사항을 만족하는 v4 전체를 만들어 주세요.
Contact : azureguy@empal.com / azureguy@cau.ac.kr