GAS×Vue.jsでスタバドリンク&カスタムを提案するアプリを作りました


この記事はVue#2アドベントカレンダーの20日目です🎅

今年がアドベントカレンダー初参加の@amakawa_です。名古屋でフロントエンドエンジニアやってます。
GAS×Vue.js×Firebaseで個人開発したアプリと使用技術について書きます！

# 前置き
## スタバの注文って難しくない？？
先月、会社の人と「スタバに行くといつも同じドリンクしか注文できない」という話になりました。
もちろん多種多様なドリンクやカスタムがあるのは知っていますが、

- カスタムの種類がわからない
- どのドリンクに組み合わせられるかわからない
- 合計金額がわからない
- どうやって頼んだら良いのかわからない

その結果、私はココアとチャイティーラテをヘビロテしています。


## ドリンクとカスタムを良い感じにしてくれるアプリがあったら良いよね

作りました
https://starpicks2019.firebaseapp.com/
(もし合計金額やカスタムが間違っていても責任は負いかねますので、ネタとして楽しんでください...)
![20191220.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267607/1bbfc2bc-a711-243b-7c98-d3201453e012.gif)


サイズと上限金額を指定すると、ドリンクとカスタムを最大3つまでランダムで提案してくれます。
サクッと作るために、条件を下記に絞りました。

- 期間限定、店舗限定メニュー除く
- ホット/アイスを区別しない
- 税抜き表示
- 味の組み合わせは無視(ほうじ茶×ホワイトモカとか出ます)
- レスポンシブにしない（スマホ表示のみ）

# 技術的なお話
## 全体の構成
主な使用技術はGAS、Vue.js、Firebaseです。

Googel Spread Sheetにドリンクやカスタムのデータを入れ、ブラウザからのリクエストに応じてGASでJSONを作成してVueにレスポンスを返すようにしました。公開にはFirebaseのホスティング機能を使用しています。
GASをAPIとして使うのは初めてでしたが、簡単なアプリのAPIを作るくらいなら非常に便利です。

使用した技術についてもう少し詳しく書いていきます

## Google App Script(GAS)
フロントエンドエンジニアが個人開発をすると、DBやサーバーサイドがネックになりがちかと思います。
FirebaseのCloud Firestoreも良いのですが、NoSQLはちょっと辛い。でもRailsでガッツリ作る規模でもない。
ということでGASを採用しました。
動作は重いのですが、jsにかなり近い感覚で書けるのでストレスが少ないです。

下記のコードは、ブラウザ(Vue)から投げられたパラメータに応じて、スプレッドシートの中から条件に合うドリンクとカスタムを探します。
e.parameterでブラウザからのパラメータを取得できるので、取り出したサイズと上限金額をgetDrinkAndCustom()に渡します。

``` js
function doGet(e){
  var maxPrice = parseInt(e.parameter.price, 10);
  var selectedSize = parseInt(e.parameter.size, 10);
  var result = getDrinkAndCustom(maxPrice, selectedSize);
  
  var out = ContentService.createTextOutput();

  var responseText = JSON.stringify(result);
  out.setMimeType(ContentService.MimeType.JSON);
  out.setContent(responseText);
  
  return out;
}
```
スプレッドシートには、ドリンクのサイズごとの価格や組み合わせられるカスタムをまとめています。


最初はスタバのサイトをスクレイピングしようとしたのですが、スクロールすると表示される部分が上手く取得できず、通年メニューということで諦めて手動でまとめました。
今回一番大変だったのはこの部分です...。

ドリンクの種類やメニューは公式サイトに載っているのですが、カスタムの組み合わせまでは分からず、スタバマニアの方々の個人サイトを参考にひたすらtrue/falseを入れ続けました。
特に[スタバマニアのブログ](https://yamaguchi-coffee.com/starbucks-manias-blog)には大変お世話になりました。全国のスタバマニアさんありがとうございます。

<img width="1437" alt="スクリーンショット 2019-12-20 1.12.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267607/5b647eff-fb8e-9710-decf-08a7f1d68ecc.png">
ドリンクによって組み合わせられるカスタムが違うので、スプレッドシートにBooleanで記録しています(黄色セル)。
しかし、ここを毎回読むと重くなるので、各ドリンクの可能カスタムをインデックスで持っています。インデックスを取得する関数は別に作成して、true/falseを書き換えたときも一発で更新できるようにしました。

```js
//金額の条件に合うドリンクをランダムで取得
function selectDrinks(allDrinkNamesAndPrice, maxPrice, selectedSize){
  const selectedDrinksArray = [];
  
  while(selectedDrinksArray.length < 3){
    var random = Math.floor(Math.random() * allDrinkNamesAndPrice.length);
    var isOrderedSizeDrinkProvided = allDrinkNamesAndPrice[random][selectedSize] !== "";
    var isDrinkUnderMaxPrice = allDrinkNamesAndPrice[random][selectedSize] < maxPrice;
    var isDrinkNameNotIncluded = selectedDrinksArray.indexOf(random) === -1;
    
    if(!isOrderedSizeDrinkProvided || !isDrinkUnderMaxPrice || !isDrinkNameNotIncluded){
      continue;
    } else {
      var selectedDrinkRow = random + 4;
      var drinkName = allDrinkNamesAndPrice[random][0];
      var drinkPrice = allDrinkNamesAndPrice[random][selectedSize];
      selectedDrinksArray.push([selectedDrinkRow, drinkPrice, drinkName]);
    };
  };
  return selectedDrinksArray;
}

```
allDrinkNamesAndPriceにはシートから取得したドリンクと金額一覧が入っています。
何度もシートを読み込むとGASは遅くなるのでまとめて取得しています。

その中から、指定されたサイズがあるか、ドリンク単体で上限金額を超えていないかなどをチェックしてドリンクを3つ選んでいます。


下記はカスタムの取得部分です。

```js
function getCustom(selectedDrinksArray, maxPrice, drinkSheet, customSheet){
  const allCustom = customSheet.getRange(1, 1, 76, 5).getValues();

  var selectedDrinkAndCustomArray = [];
  
  for(var i=0; i<selectedDrinksArray.length;i++){
    var name = selectedDrinksArray[i][2];
    var price = selectedDrinksArray[i][1];
    var row = selectedDrinksArray[i][0]
    var customIndexArray = drinkSheet.getRange(row, 7, 1, 1).getValues()[0][0].split(",");
    var customNumber = 0;
    var total = price;
    var randomArray = [];
    var selectedAllCustomArray = { name: name, price: price, custom: [] };
    
    while(customNumber < 3){
      var random = Math.floor(Math.random() * customIndexArray.length);
      var randomCustomIndex = customIndexArray[random] - 1;
      
      var customType = allCustom[randomCustomIndex][0]; 
      var customName = allCustom[randomCustomIndex][4]; 
      var customPrice = allCustom[randomCustomIndex][3]; 
      
      var isRandomIndexNotIncluded = randomArray.indexOf(customType) === -1;
      if(!isRandomIndexNotIncluded) continue;
      
      if (total + customPrice < maxPrice){
        var selectedCustomArray = { customName: customName, customPrice: customPrice };
        selectedAllCustomArray["custom"].push(selectedCustomArray);
        customNumber = customNumber + 1;
        randomArray.push(customType);
        total = total + customPrice;
      } else {
        break;
      }
    }
    selectedDrinkAndCustomArray.push(selectedAllCustomArray);
  };
  return selectedDrinkAndCustomArray;
}

```

カスタムの名前や金額は別シートにまとめたので、ここも最初にまとめて取得しています。

先ほど選んだドリンクに付けられるカスタムは、customIndexArrayで取得しているので、その中からランダムに取り出して金額チェックと重複チェックをしています。
customTypeとcustomNameの違いが分かりにくいのですが、例えば異なるcustomNameを持つ「キャラメルシロップ多め」と「キャラメルシロップなし」を同時に選ばないために、両者に「キャラメルシロップ」という同じcustomTypeを指定して重複を防いでいます。

## Vue.js
フロント構築は１週間しか時間がなかったので、普段使っている技術をメインにしてスピード重視で書きました。
それでも仕事では業務用ツールがメインなので、SVG操作など新しい技術を触れて楽しかったです。

[Vue CLI3](https://jp.vuejs.org/2018/08/11/vue-cli-3-0-is-here/)を使うことでめちゃめちゃ簡単に環境構築ができました。
Vue Router、Vuex、Eslint、Jestなどおなじみのライブラリをすぐに使えます。
（本当はTypeScriptもガッツリ使いたかったのですが、時間切れなので年末年始の宿題となりました）

### 動くSVG
![20191220.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267607/c572f8e0-1e81-455d-c18e-142c3c2d669b.gif)



ちょっと遊び心を入れてみました。
data内ののsizeプロパティが変わると、computed内のiconSizeが連動することでSVGのwidthとheightが変わるようになっています。
SVG画像は別コンポーネントとして切り出しました。

今回のSVGの操作は[Vue公式](https://jp.vuejs.org/v2/cookbook/editable-svg-icons.html)を参考にしています。複数SVG画像でのコンポーネントの再利用やアニメーションなど、ライブラリを使わないSVG操作について詳しく書いてあるので勉強になると思います。


```vue

<template>
  <div class="select">
    (省略)
    <div class="select-icon">
      <div class="select-icon-bg"></div>
      <div class="select-icon-container flex content-center flex-middle">
        <IconBase
          class="select-icon-drink"
          :width="iconSize"
          :height="iconSize"
        />
      </div>
      (省略)
    </div>
    <div class="select-slider-container">
      <div class="select-slider">
        <p class="select-slider-name">size</p>
        <div>
          <vue-slider v-model="size" :data="sizeOption" :marks="true" />
        </div>
      </div>
  (省略)
  </div>
</template>

<script>
import IconBase from "../components/IconBase.vue";
  (省略)
export default Vue.extend({
  name: "select",
  components: {
    IconBase,
    VueSlider
  },
  data() {
    return {
      size: "Tall",
      sizeOption: ["Short", "Tall", "Grande", "Venti"],
      price: 500,
      priceOption: [300, 400, 450, 500, 550, 600, 650, 700]
    };
  },
  computed: {
    iconSize() {
      switch (this.size) {
        case "Short":
          return 74;
        case "Tall":
          return 88;
        case "Grande":
          return 112;
        case "Venti":
          return 136;
        default:
          return 88;
      }
    }
  },
...
});
</script>
```

### vue-slider-component
スライダーの部分は[vue-slider-component](https://github.com/NightCatSama/vue-slider-component)を使用しました。

```vue
      <div class="select-slider">
        <p class="select-slider-name">size</p>
        <div>
          <vue-slider v-model="size" :data="sizeOption" :marks="true" />
        </div>
      </div>
(省略)
<script>
(省略)
  data() {
    return {
      size: "Tall",
      sizeOption: ["Short", "Tall", "Grande", "Venti"],
      price: 500,
      priceOption: [300, 400, 450, 500, 550, 600, 650, 700]
    };
</script>
```

スライダー状で選択した値はv-modelで選択したdataプロパティにバインディングされ、スライダーのオプションも配列で指定されるので、データの受け渡しもとても簡単です。

また、ツールチップなどのデザインも細かくカスタムできます。単純な色指定ならCSSの上書きで可能なのでDevツールを見ながらガンガン変えてみました。
[ドキュメント](https://github.com/NightCatSama/vue-slider-component)も丁寧なので、興味のある方はぜひご覧ください。

個人的には、今後もスライダー使うならコレ！と思うくらい好きです。

# 完成！
以上、GAS×Vueでスタバドリンク&カスタムを提案するアプリでした。
GASがメインみたいな内容になってすみません。

今回アドベントカレンダーという期限付きだったこともあり、かつてないスピードで構築＆アプリ公開できました。
とりあえず公開するのは大切ですね。

# 今後やりたいこと
- TypeScript導入（仕事でTS触る予定があるので練習用に書くつもりだった）
- ローディング追加（やっぱりGASは遅い）
- 税込表示
- ホット/アイス選択
- 検索条件に「甘さ」を追加
    - ドリンクやカスタムごとに重み付けをしておいて、甘さレベルが選べたら嬉しい
- 選んだドリンク＆カスタムをOGP画像にする
    - 本当にただやってみたい


明日のアドベントカレンダーは@tikin0716さんです！！🎄