Gemini で GAS の web アプリをつくる

:::note
**新入社員向けに生成 AI を活用した開発に触れてもらうに、研修用の手順書を作成しました。**
:::

## スプレッドシートのテーブルデータと連携させた web アプリをつくります
   
  
### 補足：GAS のこれから
Gemini が Google Workspace に統合される等、アプリのデプロイ環境の充実にも期待ができそうですが、Google Workspace のアドオンは GAS で開発されたものも多く、現状のエコシステムを支えていると言えます。「Gemini でコードを生成し、GAS でコードを書きデプロイする」というフローはややスマートでない感じもしますが、GAS がなくなるというよりかは、Gemini との連携が強化されていくような流れになるのではないでしょうか？
　
## 実装

以下のステップで実装します。

:::note
1. スプレッドシートでデータを用意する
1. GAS の web アプリの開発を Gemini で指示する
1. スプレッドシートと連携させる
1. デプロイ
:::

### 1. データを用意する
スプレッドシートで時系列のデータを用意します。何かの実験データがよいです。  
データが無ければ、Gemini でつくっても構いません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/b8189f05-0713-43cc-90b7-fc0ed8e1794d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/0b5665eb-f988-446a-8064-355e63d19ed5.png)

### 2. GAS の web アプリの開発を Gemini で指示する

:::note alert
会社のガイドラインに従って Gemini などの生成 AI サービスを活用してください。
わからない場合は確認しながら進めましょう。
:::

スプレッドシートの`拡張` (`Extensions`)から`Apps Script` を選びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/7cee95f9-8560-46f4-a7ae-97b119d3c186.png)

GAS(Google App Script)のウィンドウが開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/d74f717f-4a96-4cd2-8b8d-7227fe7c3392.png)

コーディングを始めます。Gemini につくって欲しいアプリの内容を伝えましょう。  
  
次のプロンプトを参考にして、ご自身でカスタマイズしてください。

```
スプレッドシートのデータを使ってGASのアプリをつくってください
# 体重管理・将来予測アプリの作成
スプレッドシートに記録されたユーザーの日付ごとの体重データと将来の体重が予測されるチャートが表示されるアプリをつくってください。

## 必要な機能
1. ユーザー入力:
    - ユーザーの性別
    - ユーザーの身長
    - 連携の実行

2. 計算機能:
    - スプレッドシートに入力された値に基づいて、1か月後の体重を計算する

3. 表示機能:
    - 1か月後の体重を表示
    - 日にちの経過に伴う体重の変化をグラフで表示

## デザイン要件
    - モバイルフレンドリーなレスポンシブデザイン
    - シンプルで使いやすいUI
    - 結果は視覚的に分かりやすく表示

## 技術的な詳細
    - 体重は食事等で一日でプラスマイナス1kg変動することがある

## 言語
    - 日本語のUIで作成

## 開発環境
    - Google App Script でwebアプリとしてデプロイする
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/849db8ae-a898-44df-a877-a9e1a83f4bcd.png)


コードを生成してくれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/67cd939f-b462-46de-a6dd-112fcbca7116.png)

Gemini の Canvas 機能でプレビューすることもできるので、思った通りのアプリになっているか確認しながら適宜修正を入れていきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/52b88d04-9597-4352-9f88-02960101f4f2.png)

<details><summary>生成したコード</summary>

```js:Code.gs
const SHEET_NAME_DATA = 'データシート';
const WEIGHT_FLUCTUATION = 1; // 一日の体重変動幅（kg）

function doGet(e) {
  return HtmlService.createTemplateFromFile('index')
      .evaluate()
      .setTitle('体重管理・将来予測アプリ')
      .setFaviconUrl('https://ssl.gstatic.com/docs/script/images/favicon.ico');
}

function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

function getWeightData() {
  const dataSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME_DATA);
  const lastRow = dataSheet.getLastRow();
  if (lastRow < 2) {
    return [];
  }
  const dataRange = dataSheet.getRange(2, 1, lastRow - 1, 2);
  const values = dataRange.getValues();
  return values.map(row => ({ date: Utilities.formatDate(new Date(row[0]), Session.getTimeZone(), 'yyyy-MM-dd'), weight: row[1] }));
}

function predictWeight(gender, height) {
  const weightData = getWeightData();
  if (weightData.length === 0) {
    return { predictedWeight: 'データがありません', chartData: [] };
  }

  const latestWeight = weightData[weightData.length - 1].weight;
  const predictedWeight = latestWeight + (Math.random() * 2 * WEIGHT_FLUCTUATION - WEIGHT_FLUCTUATION); // ランダムな変動を加味

  // 過去30日間のデータと予測データをチャート用に整形
  const chartData = [['Date', 'Weight']];
  const startDate = new Date();
  startDate.setDate(startDate.getDate() - 30);

  const filteredData = weightData.filter(item => new Date(item.date) >= startDate);
  filteredData.forEach(item => {
    chartData.push([item.date, item.weight]);
  });

  // 30日後までの予測データを追加 (簡易的な直線予測)
  const lastDate = new Date(weightData[weightData.length - 1].date);
  for (let i = 1; i <= 30; i++) {
    const nextDate = new Date(lastDate);
    nextDate.setDate(lastDate.getDate() + i);
    const predictedNextWeight = latestWeight + (Math.random() * 2 * WEIGHT_FLUCTUATION - WEIGHT_FLUCTUATION) * (i / 10); // 徐々に変動幅を大きく
    chartData.push([Utilities.formatDate(nextDate, Session.getTimeZone(), 'yyyy-MM-dd'), predictedNextWeight]);
  }

  return { predictedWeight: predictedWeight.toFixed(1), chartData: chartData };
}
```

```html:index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>体重管理・将来予測</title>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
  <style>
    body {
      font-family: sans-serif;
      padding: 20px;
    }
    .container {
      max-width: 600px;
      margin: 0 auto;
    }
    .form-group {
      margin-bottom: 20px;
    }
    #result {
      margin-top: 30px;
      padding: 20px;
      border: 1px solid #ccc;
      border-radius: 5px;
      background-color: #f9f9f9;
    }
    #chart_div {
      margin-top: 30px;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>体重管理・将来予測</h1>
    <div class="form-group">
      <label for="gender">性別:</label>
      <input type="text" class="form-control" id="gender">
    </div>
    <div class="form-group">
      <label for="height">身長 (cm):</label>
      <input type="number" class="form-control" id="height">
    </div>
    <button class="btn btn-primary" onclick="runPrediction()">連携を実行</button>

    <div id="result" style="display:none;">
      <h2>予測結果</h2>
      <p>1か月後の体重（予測）： <span id="predictedWeight"></span> kg</p>
    </div>

    <div id="chart_div" style="width: 100%; height: 400px; display:none;"></div>
  </div>

  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
  <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
  <script>
    $(document).ready(function() {
      loadUserInfo();
    });

    function loadUserInfo() {
      google.script.run.getUserInfo().then(function(userInfo) {
        $('#gender').val(userInfo.gender);
        $('#height').val(userInfo.height);
      });
    }

    function runPrediction() {
      google.script.run.predictWeight().then(function(result) {
        $('#predictedWeight').text(result.predictedWeight);
        $('#result').show();
        if (result.chartData && result.chartData.length > 1) {
          drawChart(result.chartData);
          $('#chart_div').show();
        } else {
          $('#chart_div').hide();
        }
      });
    }

    function drawChart(chartData) {
      google.charts.load('current', {'packages':['corechart']});
      google.charts.setOnLoadCallback(function() {
        var data = google.visualization.arrayToDataTable(chartData);

        var options = {
          title: '過去30日と将来30日の体重変化',
          hAxis: { title: '日付' },
          vAxis: { title: '体重 (kg)' },
          legend: { position: 'bottom' },
          curveType: 'function'
        };

        var chart = new google.visualization.LineChart(document.getElementById('chart_div'));
        chart.draw(data, options);
      });
    }
  </script>
</body>
</html>
```

</details>

コードができたら、GAS の方に書いていきましょう。  
ちなみに、私が試しにつくったサンプルアプリは、`Code.gs` と `index.html` の2つのファイルがあるので、それらを実装します。

`Code.gs` は GAS を開いた時に初めから用意されているので、それを書き換えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/48a4f331-50fc-4c4f-a817-4730fae0a93e.png)

続いて、左のサイドバーから `HTML` のファイルを追加し、`index.html` という名前を付けます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/62f7615e-4fb0-488f-a7aa-284a1fbab261.png)

`index` という名前だけ付ければ拡張子(.html)は自動で付与されます。
  
同じようにコードを貼り付けます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/b5028eb7-0f01-47f5-ba93-ccbbff15546d.png)

一度プロジェクトを保存しましょう。保存のアイコンかショートカットキーを使います。  
保存がされていない時は、ファイルの左側にオレンジ色のマークが出ます。
  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/24648b96-6379-4e97-8251-b46dada28b4f.png)

### 3. スプレッドシートと連携させる

最後のステップです。最初につくったスプレッドシートと連携させましょう。
ここから先は生成されたコードによりやることが変わってきます。
少しコードの中身にも触れておきたいので、以下のどちらになっているか確認してみましょう。  

:::note
1.　現在開いているスプレッドシートを取得している
2.　スプレッドシートの ID を指定して取得している
:::

この辺りも、Gemini に次のように聞けば教えてくれます。この辺りの流れがわかっている方は、コードの生成段階でやり方を指定しておくとよいです。

```
スプレッドシートの ID など、データを連携させるためにコードに書き換える必要がある個所を教えてください。もしなければその理由を教えてください。
```

#### 1. 現在開いているスプレッドシートを取得している
上述した私のサンプルコードはこのパターンです。
この GAS はスプレッドシートのプロジェクトから立ち上げたため、`SpreadsheetApp.getActiveSpreadsheet()`でスプレッドシートの内容を取得します。

ただし、以下の画像のようにシート名を指定しているので、スプレッドシートのシート名を変えるか、コードを現在のスプレッドシートのシート名に書き換えるか、をして指定するシート名を揃えてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/af5f3d12-2e72-4249-9880-1ff7329fb10b.png)

#### 2. ID を指定して取得している

この場合は、`SpreadsheetApp.openById()`で取得します。ID というのはスプレッドシートに割り振られたユニーク(固有)な値で、以下の URL の`XX...XX`の部分になります。 

`https://docs.google.com/spreadsheets/d/XXXXXXXXXXXXXXXXXXXXXXXXXXX`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/3b38bb72-bb66-4029-b2cf-bcc0712d6ace.png)

<details><summary>サンプルコード</summary>

```js:Code.gs
const SHEET_NAME_DATA = 'データシート';
const WEIGHT_FLUCTUATION = 1; // 一日の体重変動幅（kg）
const SPREADSHEET_ID = 'ここにあなたのスプレッドシートIDを入力してください'; // ★ IDをここに入力

function doGet(e) {
  return HtmlService.createTemplateFromFile('index')
      .evaluate()
      .setTitle('体重管理・将来予測アプリ')
      .setFaviconUrl('https://ssl.gstatic.com/docs/script/images/favicon.ico');
}

function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

function getWeightData() {
  const ss = SpreadsheetApp.openById(SPREADSHEET_ID); // IDでスプレッドシートを開く
  const dataSheet = ss.getSheetByName(SHEET_NAME_DATA);
  const lastRow = dataSheet.getLastRow();
  if (lastRow < 2) {
    return [];
  }
  const dataRange = dataSheet.getRange(2, 1, lastRow - 1, 2);
  const values = dataRange.getValues();
  return values.map(row => ({ date: Utilities.formatDate(new Date(row[0]), Session.getTimeZone(), 'yyyy-MM-dd'), weight: row[1] }));
}

function predictWeight(gender, height) {
  const weightData = getWeightData();
  if (weightData.length === 0) {
    return { predictedWeight: 'データがありません', chartData: [] };
  }

  const latestWeight = weightData[weightData.length - 1].weight;
  const predictedWeight = latestWeight + (Math.random() * 2 * WEIGHT_FLUCTUATION - WEIGHT_FLUCTUATION); // ランダムな変動を加味

  // 過去30日間のデータと予測データをチャート用に整形
  const chartData = [['Date', 'Weight']];
  const startDate = new Date();
  startDate.setDate(startDate.getDate() - 30);

  const filteredData = weightData.filter(item => new Date(item.date) >= startDate);
  filteredData.forEach(item => {
    chartData.push([item.date, item.weight]);
  });

  // 30日後までの予測データを追加 (簡易的な直線予測)
  const lastDate = new Date(weightData[weightData.length - 1].date);
  for (let i = 1; i <= 30; i++) {
    const nextDate = new Date(lastDate);
    nextDate.setDate(lastDate.getDate() + i);
    const predictedNextWeight = latestWeight + (Math.random() * 2 * WEIGHT_FLUCTUATION - WEIGHT_FLUCTUATION) * (i / 10); // 徐々に変動幅を大きく
    chartData.push([Utilities.formatDate(nextDate, Session.getTimeZone(), 'yyyy-MM-dd'), predictedNextWeight]);
  }

  return { predictedWeight: predictedWeight.toFixed(1), chartData: chartData };
}
```

</details>

1.と同様、シート名を変える必要があれば編集してください。

### 4. デプロイ
最後にプロジェクトを web 上に公開します。

`Deploy` から `New Deployment` を選びます。

> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/46c51de5-1f00-426f-a47b-06ea2d74f256.png)

設定アイコンから `Web app` を選びます。
> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/7303f0fc-0775-4bbf-850c-163aa17b1e45.png)


そのままデプロイに進むと認証許可を求められます。
> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/67047c17-5f1f-4624-9d33-21509f220490.png)

公開範囲を指定できます。ここではアクセスできるのは自分だけにしておきましょう。
> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/5159ba99-7b3d-45af-9576-45f13fb115c1.png)


`Authorize access` から認証許可を進めていくと URL が発行されます。
> 下の方に Web App にアプリの URL が用意されています。
> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/d7b1579b-73f9-433b-a500-36fb35f2a678.png)

URL を開くとアプリができています。思った通りの動作になるか確かめてみてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/6dfc75bc-bc6b-4961-96a9-8055fef2fdce.png)


## Tips:1. エラーが出た時  
思った通りにならなくても心配しないでください。

例えば、画面上にエラーメッセージが表示されていたら、そのまま、 Gemini に内容を伝えて修正してもらいましょう。Canvas のプレビューも使ってください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/abd76df0-791c-4902-b7ad-b516a56c56e5.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/74112f47-788e-4dd4-999e-ba6710223a46.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/0a841140-2651-4646-bdbc-1d59aa18e844.png)

エラーメッセージでなくても、現象を伝えれば解決してくれることがありますが、効果性の回答を引き出すことは意図しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/1da3a939-26f4-475c-9bed-781bad7c7920.png)

コードを直したら、また `New Deployment` でデプロイしなおせばOKです！

## Tips:2. 生成 AI でWeb アプリをつくれるサービス    
他にもいろいろなサービスがあります。

### Replit
Gemini からそのままエキスポートできるので LP をつくる際などにも使いやすいです。

https://replit.com/

### v0
Vercel社が提供するサービスです。UIデザインとコード生成ができます。

https://v0.dev/

### Devin
AI エージェントです。GitHub のリポジトリ上で作業します。Slack などのチャットアプリを使いながら開発ができます。

https://devin.ai/

Devin については記事を書いたので気になる人は読んでください！

https://qiita.com/yutowac/items/cec702100e0e628f9e62


## まとめ  
自身が集めたデータ使って、ユーザーが有効活用できるようなアプリを考案できたでしょうか？
つくって見ると「そもそもこの機能必要ないんじゃない？」など発見があるかもしれません。使ってみてのフィードバックがすぐに得られるのはこのような開発方法のよい点ですね！