33
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

京都のテイクアウト情報が集まるサービスを作った(GAS)

Last updated at Posted at 2020-04-28

更新

2020/05/05 更新

  • ありがたいことに、思っていたよりもLGTMしていただけたため、流石に色々省きすぎだなと思い少し追記しました。
  • 変更点
    • webpackについて追記
    • Firestoreへの投入部分の追記

はじめに

大学の友人数名と京都のテイクアウト情報が集まるサービスを作りました!
https://kyoto-take-out.web.app/
京都に住んでいる人はぜひ使ってみてください👘
ツイッターも拡散していただけると幸いです🙇‍♂️
https://twitter.com/LN_shono/status/1253324958601637890

このサービスで使用したものはざっくりとこんな感じです。

  • Google スプレッドシート
  • Google フォーム
  • GAS
  • React
  • TypeScript
  • Firestore

今回私は、GASを使ったお店情報の収集・編集・firestoreへの投入などの開発を担当したので、やったことを簡単にまとめます。

併せてスプレッドシートとGitHubもご覧ください。
スプレッドシート
GitHub

サービス構成

このサービスではお店の情報をGoogleフォームを用いて収集しています。
そしてフォームで集まった回答を、GASで集計・firestoreに投入し、
フロントエンドはfirestoreの情報を表示しています。
つまり、情報の流れは以下のようになっています。

  1. お店がフォームに回答
  2. スプレッドシートに回答が反映
  3. GASで回答をfirestoreに投入する形に整形
    1. 緯度経度を取得等もここ
  4. GASでfirestoreを更新
  5. フロントエンドはfirestoreのデータを表示

スプレッドシート & GAS

概要

スプレッドシート(GAS)は主に以下のような役割を担っています。

  • ログの保存
  • 住所からの緯度経度の取得
  • 編集機能
  • firestoreへのデータ投入

各シートの説明

フォームの回答 1

これは名前のままで、フォームの回答が反映されるシートです。
上のスプレッドシートではフォームを作成していないのでただのシートですが、
実際に使っているものはフォームとリンクしており、
フォームが回答されるとリアルタイムで反映されます。
また、フォームの設定では回答の編集を許可しており、フォームの回答が編集されると該当する部分の値が変更されます。

入力データログ

このシートはフォームへの入力をログとして保存しています。
基本的にはフォームの回答 1と同じ内容にはなるのですが、編集された場合に前のデータが消えてしまうことを防ぐ目的で作成しています。
トリガーを設定しており、フォームから新しい回答が来たり編集されたりするとこのシートが更新されます(後述)。

firestore投入用シート

このシートは最終的にfirestoreに保存されるデータを表示しています。
そして同時に、スプレッドシートを編集することで、firestoreに入力するデータを編集することができるようにしています。
このようにしている理由としては、フォームの入力が誤っていたり、少し見た目を整形したい場合などに、開発側でデータを確認・編集したいことがあるからです。

スプレッドシートを見ていただけるとわかるのですが、一部のカラムに同じ名前+(編集済み)同じ名前+(過去の編集)といったカラムを用意することで編集を可能にしています。

詳しくは後述します。

シートの更新

入力データログの更新

index.tscreateInputLogSheetに対応します。
やっていることは簡単で、フォームの回答 1入力データログの内容をそれぞれ取得しIDを比較して、

  • 入力データログになくフォームの回答 1にあるデータ(つまり新しい回答)
  • 入力データログにもあるがフォームの回答 1の方が新しいデータ(つまり編集された回答)

入力データログに挿入します。
フォームの回答が編集されるとタイムスタンプも更新されることを利用しています。

firestore投入用シートの更新

index.tscreateStoredSheetに対応します。
こちらも上と基本的にやっていることは変わりませんが、

  • 挿入ではなく、シートを毎回作り直している(正確には全てのデータを消した後に挿入している)
    • 最新版のみ表示したい & firestoreに入っているデータと同期させていたいのでこのような形にしています。
  • 緯度経度取得の処理を挟んでいる
  • 編集用の処理を挟んでいる

などの違いがあります。

編集用の処理について補足します。

基本的に、(編集済み)とついた列に入力があればそちらを優先してfirestoreに投入するようにしています。
そして、(編集済み)の列はフォームに更新があった際もそのまま残るようにしています。

しかし、このままですとテイクアウトメニューや営業日時に変更があった場合にデータが更新されないことになってしまいます。そこで、(過去の編集)といった列も用意し、フォームに更新があった際には(編集済み)の値を(過去の編集)に移動させることで、店舗による新しい入力を保存する & 編集履歴として確認できるようにする、といったことを可能にしています。

画像URLや緯度経度などは店舗側が更新することがほとんどないだろうと思い(編集済み)のみになっています。

また、TwitterURLなどは(編集済み)の列はないのですが、編集したデータを更新時も優先するようになっています。
これも更新されることがまずないだろうという考えでそうしています。

Firestoreへのデータ投入(2020/05/05 追記)

index.tssheetToFirestoreに対応しています。
以下のことをしています。

  • firestore投入用シートのデータを取得
  • Firestore用のデータへと整形
  • isNew,needUpdatingの値によって追加 or 更新
    • isNewがtrueの時は追加、isNewがfalse かつ needUpdatingがtrueの時は更新、どちらもfalseであれば何もしない
  • isNewneedUpdatingを全てfalseに

コードの話は後述します。

コードの補足

スプレッドシートからのデータの取得

スプレッドシートからのデータの取得はJSONなどのように綺麗にはできないので、どうすれば一番綺麗で楽かなぁ、と悩みました。
最終的にはこのような形が一番楽かなと思いました。
スプレッドシートに列を追加する際は

  1. スプレッドシートを変更する
  2. モデルに要素を追加する
  3. (1)でエラーが出るので修正
  4. (2)を修正

とすることで、いい感じに修正できます。

ただこれでも、列名を変えてしまうと動きません。
また、処理時間の制限が厳しいときには使えないです。
(スプレッドシートからの取得に時間がかかる)

コード
spreadsheetService.ts
export class SpreadsheetService {
  spreadsheetID: string;
  constructor() {
    this.spreadsheetID = OriginalUtilities.getProperty("SPREADSHEET_ID");
  }

  // (2)
  getIndexesOfFormAnswer = (firstRow: string[]) => {
    return {
      timestamp: firstRow.indexOf("タイムスタンプ"),
      email: firstRow.indexOf("メールアドレス"),
      storeName: firstRow.indexOf("店舗名"),
      ...
    };
  };

...

  getDataFromFormAnswer = (): SpreadsheetModel.FormAnswer[] => {
    const sheetName = "フォームの回答 1";
    const sheet = this.getSheetByName(sheetName);

    const firstRow = this.getFirstRowFromSheet(sheet);

    const data = sheet
      .getRange(2, 1, sheet.getLastRow() - 1, firstRow.length)
      .getValues();

    const indexOf = this.getIndexesOfFormAnswer(firstRow);

    for (const key in indexOf) {
      if (indexOf[key] < 0) {
        console.log(`[${sheetName}]に[${key}]が存在しません`);
      }
    }

    const arrayOfAnswer: SpreadsheetModel.FormAnswer[] = [];
  	// (1)
    data.forEach((row, index) => {
      arrayOfAnswer.push({
        ID: index + 1,
        timestamp: row[indexOf.timestamp],
        email: row[indexOf.email],
        storeName: row[indexOf.storeName],
        ...
      });
    });
    return arrayOfAnswer;
  };

...

  getFirstRowFromSheet = (sheet: GoogleAppsScript.Spreadsheet.Sheet) => {
    const indexOfLastColumn = sheet
      .getRange(1, 1)
      .getNextDataCell(SpreadsheetApp.Direction.NEXT)
      .getColumn();
    const firstRow = sheet.getRange(1, 1, 1, indexOfLastColumn).getValues()[0];

    return firstRow;
  };
}
SpreadsheetModel.ts
export interface FormAnswer {
  ID: number;
  timestamp: string;
  email: string;
  storeName: string;
  address: string;
  telephoneNumber: string;
  takeoutMenu: string;
  twitterURL: string;
  instagramURL: string;
  tabelogURL: string;
  onlineSalesURL: string;
  imageURL: string;
  businessHour: string;
  facebookURL: string;
  homepageURL: string;
}

.dev-clasp

普段GASの開発ではclaspを使っています。
普通に使うと一つのフォルダが一つのGASプロジェクトに対応するのですが、テスト環境で使ってから本番に反映したい、といったときもありますよね。
そこで、.dev-clasp.jsonpushDev.shというスクリプトを用いて、package.jsonscriptsでdev用と本番用でスクリプトを分けました。

pushDev.sh
#!/bin/sh
cd `dirname $0`

mv .clasp.json .clasp-renamed.json
mv .dev-clasp.json .clasp.json

clasp push

mv .clasp.json .dev-clasp.json
mv .clasp-renamed.json .clasp.json

webpack(2020/05/05 追記)

参考

claspはwebpackでbuildしなくてもTypescriptを変換してpushしてくれるのですが、node_modulesを自動的に含めるといったことはしてくれません。(そもそもimportという概念がGASになく全てグローバルといった感じ(のはず))
そこで、momentなどのパッケージを使うときはwebpackを使ってbuildするようにするとうまく出来ます。
GASでもライブラリが使える(momentもあり同じように使える)のですが、ローカルで開発する際は補完等出来なかったり、エラーが表示されたりしてしまいます。
ただ「絶対webpackを使用すべき」という訳ではないと思っていて、それぞれ一長一短あるので使い分けかなと思っています。
基本的にはclasp単体で事足りると思います。
(むしろ事足りないのであれば、GASでやらない方が良さそう)
今回は実際やってみたらどんな感じなのか知りたくて使ってみました。

clasp単体 webpack併用
難易度 簡単 少し面倒
node_module 使えない(ただしGASのライブラリがある) 使える
ただしGASで使えない機能を使っているものは使えない
ライブラリの補完 基本的に使えない
後述する型定義ファイルを用意すれば使える
使える
pushにかかる時間 短い buildを挟むので少し長い
GAS上での見た目 基本的にそのままのコードなので、GASエディタ上でも編集できる bundleされたファイルなので、GASエディタ上では見にくい
ファイルサイズも大きくなる

Firestoreへのデータ投入(2020/05/05 追記)

sheetToFirestoreについて、もう少し具体的に解説します。
上でも少し触れているのですが、GASには便利なライブラリが数多く存在しています。
実際に、sheetToFirestoreでもFirestoreAppFirestoreGoogleAppsScript)というライブラリを使用しています。
使用方法は簡単で、

  1. GASエディタにアクセス
  2. リソース>ライブラリを選択
  3. 1VUSl4b1r1eoNcRWotZM3e87ygkxvXltOgyDZhixqncz9lQ3MjfT1iKFwを入力し追加

image.png

をするだけで、使えるようになります。
ただ、これだけですとローカル環境ではこのライブラリの存在を知らないので、補完等も出来ずエラーが出てしまいます。
そこで、型定義ファイルを作成することでライブラリを使用しつつTypescriptの利点を活かした開発を可能にします。
と言ってもやることは簡単で、srcフォルダにtypesフォルダを作成し(typesフォルダは必須ではない)、以下のFirestoreApp.d.tsを作成すると、補完が出てくるようになり、GASでもちゃんと動きます。

他のGASライブラリを使用する際も同じように、GASライブラリの名前と同一(正確にはGASエディタ上のライブラリに設定した名前?)の型定義を作成することで、開発がやりやすくなると思います。

/src/types/FirestoreApp.d.ts
declare class FirestoreApp {
  static getFirestore(email: string, key: string, projectId: string): Firestore;
}

declare class Firestore {
  getDocuments(path: string): number;
  createDocument(path: string, fields: object): any;
  updateDocument(path: string, fields: object, mask: boolean): any;
}

おわりに

今回はスプレッドシート、フォーム、GASを用いてデータ収集・編集・投入をするものを作成しました。
このまま使うことはないでしょうが、色々と応用はできるのではないかなと思います。

何かお役に立てれば幸いです。
ありがとうございました。

33
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?