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

---

## 1. Regression 추가 따라하기

- Google DocsAdvertising_Data > 사본 만들고 > 각자 드라이브에 저장

    - 데이터 시트를 `Data_Lake`로 변경 (대소문자 정확히)

        - 아래 두 코드로 Apps Script 생성

            - Code.gs 복사 & 붙여넣기

[Code.gs](https://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](https://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>
```

- 추가된 `회귀분석(버짓제안)`  확인 

    - `회귀분석 실행`  클릭

![Image](https://upload.cafenono.com/image/slashpageHome/20251205/225902_kWsQqGRSZ9f2kLHlTI?q=80&s=1280x180&t=outside&f=webp)

- `회귀분석 결과` 확인

![Image](https://upload.cafenono.com/image/slashpageHome/20251205/231040_G6DBpApnBaRKC2lr7n?q=80&s=1280x180&t=outside&f=webp)

- `예산 시나리오 (3가지 전략)`  > `총 예산 ($) : 10,000`  입력 > `3가지 전략 시나리오 제안`  클릭

![Image](https://upload.cafenono.com/image/slashpageHome/20251205/230726_yGWfhoE6HO6gTIyfND?q=80&s=1280x180&t=outside&f=webp)

- `회귀분석 결과`  내용이 `model_summary` 시트에 저장된 거 확인

![Image](https://upload.cafenono.com/image/slashpageHome/20251205/231337_mpYN8eykgtWxV79ZfJ?q=80&s=1280x180&t=outside&f=webp)

- `예산 시나리오 (3가지 전략)` 이 `optimizer_ui` 시트에 저장된 거 확인

![Image](https://upload.cafenono.com/image/slashpageHome/20251205/231541_8L7oOAMw6MnsK208qn?q=80&s=1280x180&t=outside&f=webp)

---

---

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

- 아래와 같은 단계별 워크 플로우로 AI와 직접 티키타카하면서 만들어 보기

    - Google DocsTerm_P-advertising_data_v3-Code.gs Google DocsTerm_P-advertising_data_v3-Code.gs

    - Google DocsTerm_P-advertising_data_v3-Dashboard.html    

### ✅ **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 전체를 만들어 주세요.
```

For the site tree, see the [root Markdown](https://slashpage.com/lion.md).
