ZeroScript

ゼロからわかるスクリプト

【GAS】スクリプトエディタを開く3ステップ|自動化の第一歩|ゼロからわかる

f:id:tabemi:20210528142314p:plain

どうも、たべみです。

  • Google Apps Script ってどこに書くの?
  • 業務自動化したいけど、何から始めていいのかわからない!


そんな方に、まずは業務を自動化するための場所(スクリプト エディタ)の準備方法を解説いたします。

とくに自動化業務の多い、スプレッドシートを例にご説明いたします。

大まかな手順

Googleスプレッドシートから、プログラムを貼り付ける場所である「スクリプトエディタ」を開くまでの大まかな手順です。

  1. Googleドライブを開く
  2. 新しいGoogleスプレッドシートを開く
  3. スクリプトエディタを開く

=>完了

以上が、大まかな手順です。

既存のシートでプログラムを書きたい方は、3.の手順から取り掛かってみてくださいね。

1.Googleドライブを開く

では、作業を始めていきましょう。

1-1.作業概要

  1. Googleドライブにアクセス
  • ログインを求められたらログイン
  • Googleアカウントを持っていない場合は新規作成

1-2.Googleドライブにアクセス

Google Apps Script を使用するために、Googleドライブを開きます。

まずは、Googleドライブにアクセスしてみましょう。

Googleドライブの画像
Googleドライブ

この画面が出れば成功です!

1-3.ログインを求められたらログイン

ログイン画面が出てきた方はログインしましょう。

Googleログイン画面
ログイン画面が出てきたらログインしましょう

1-4.Googleアカウントを持っていない場合は新規作成

Google Apps Script を使用するには、Googleアカウントが必須となります。

とはいっても、無料で取得できるアカウントなので安心です。

「アカウントを作成」をクリックして新しく作成しましょう。
作り方は、こちら↓です

support.google.com
また、こちらの解説が大変分かりやすいので参考にされると良いかもしれません。
Googleアカウントの作成方法を丁寧に全解説【PC・スマホ】 | アクセス中古ドメイン

ちなみに、名前は本名でなくて大丈夫ですよ。



2.新しいGoogleスプレッドシートを開く


続いて、Googleスプレッドシートを開きましょう!自動化の舞台となるものですね。

作業概要

  1. 右上の「新規+」をクリック
  2. スプレッドシートを選択

画面左上の「+新規」をクリックし、「Googleスプレッドシート」を選択して下さい。

新規ファイルの作成の画像
新規ファイルの作成
f:id:tabemi:20210523162249p:plain:w200
スプレッドシートを選択する。

これでGoogleスプレッドシートが作成できました。簡単。涙出てきます。

Googleスプレッドシートの画像
Googleスプレッドシート

「無題のスプレッドシート」とタイトルがついているかと思われますので、お好きなタイトルに変更すると良いかもしれないですよ。

3. スクリプトエディタを開く

では、最後にプログラムを書く場所、スクリプトエディタを開きましょう。

作業概要

  1. Googleスプレッドシートの画面上部、「ツール」をクリック
  2. 上から2番目の「<> スクリプトエディタ」を選択
f:id:tabemi:20210523162415p:plain
スクリプトエディタを開く方法

それっぽい画面が出てくればOKです!

スクリプトエディタ
スクリプトエディタ

試しにプログラミングしてみる

せっかく開いたのですから、なにか出力してみましょう。

スクリプトエディタを編集する

こちらをコピペしてみましょう。

function myFunction() {
  const sheetName = SpreadsheetApp.getActiveSpreadsheet().getName();
  console.log("このスプレッドシートの名前は" +sheetName +"です");
}

ちなみに、このブログ ZeroScript ではコピペでできる業務改善ツールを作成しています。関心のある方は、ぜひのぞいてみてください。

tabemi.hatenablog.com


コードを保存する

コードを貼り付けると、ファイル名のところに赤いポチが付いたのに気が付きましたでしょうか?

これはコードが未保存ということなので、保存してあげます

  1. Windows=>「Ctrl+s」、Macの方「Comd+s」

これで赤いポチがなくなったかと思います。

コードを実行してみる

再生ボタンのようなアイコンがあるかと思います。押してみます。

f:id:tabemi:20210523162409p:plain:w400
実行ボタンをクリック

すると、、ウィンドウが出てきますね。。

f:id:tabemi:20210523162329p:plain:w450
ウィンドウが出てくる。「権限を確認」をクリック。キャンセルは押しません。

承認画面を承認する

これは、「あなたに代わってプログラムが色々するけど、大丈夫かい?」という許可をGoogleが求めてきています。許可されない場合、このプログラムは動きませんので、承認しましょう

なにか情報を抜き取るコードなんじゃないの?!

大丈夫です笑

以前私は本気で心配していたんですよね。

だから何度でも言います。大丈夫です。


この認証画面をクリアする手順は、こちらです。

  1. Googleアカウントを選択
  2. 左下「詳細」>「 安全でないページに移動」をクリック
  3. 下にスクロール「許可」をクリック
f:id:tabemi:20210523162340p:plain:w200
Googleアカウントを選択
f:id:tabemi:20210523162346p:plain:w350
詳細をクリック
f:id:tabemi:20210523162354p:plain:w350
焦りますが、「安全ではないページに移動」をクリック
f:id:tabemi:20210523162404p:plain:w350
スクロールして「許可」を選択


そしたら、コードが実行できるようになっているので、
もう一度「再生ボタン」を押してみましょう

エディタ画面
出てくる表示は少し違います。

黄色く「実行完了」が出ていればOKです!
でない、という方は、もう一度▶ボタンを押してみましょう。

コピペしたコードにはスプレッドシートの名前を出力するプログラムがかかれていました。

「実行完了」の上の行に出力されているので、スプレッドシートの名前と照らし合わせてみてくださいね。

今回は、以上となります。

お疲れ様でした!!

最後に

今回は、Google Apps Script で自動化を行うために、スクリプトエディタの開き方を解説しました。

よい自動化ライフをお送りください!

ではでは~

【コピペ】6秒でアイキャッチ画像を自動生成する方法

f:id:tabemi:20210527211739p:plain
これも自動生成した

どうも、たべみです。

突然ですが、アイキャッチ作成って大変ですよね。。

そこで、、

///////////////////////////////////////

全ブロガーに告ぐ!!!
アイキャッチ作成時間を放棄せよ!

\\\\\\\\\\\\\\\\\\//////////////////////



ふう。
言ってみたかった。。


今日は、画像に自動でテキストを乗せるスクリプトをご紹介します。

簡単なアイキャッチ画像を自動で作って時短していきましょう。

完全コピペでOKです。


できること


素材の画像にテキストを挿入し、png形式で保存する

これが今回のツールでできることとなります。

f:id:tabemi:20210527223946p:plain
こんな感じに表を埋めると、
f:id:tabemi:20210527223940p:plain
画面右側でアイキャッチ画像が生成されていきます


挿入したい文字は、

  • 改行OK
  • 文字の大きさ自動調整

できます。

なーーーんも考えずにボタンを押すだけで、アイキャッチ画像を作成できます!


準備するもの

自動化するためにGoogleドライブとGoogleスプレッドシートを使用します。

それらを用意し、
コードをコピペする準備をします。

Googleスプレッドシートを新規で作成する

それではスプレッドシートを準備しましょう。

  1. Googleドライブを開く
  2. 画面左上の 新規+ ボタンをクリック
  3. スプレッドシートを選択
f:id:tabemi:20210523162245p:plain:w500
新規+ ボタンをクリック
f:id:tabemi:20210523162249p:plain:w350
スプレッドシートを選択


これで新しいスプレッドシートが作成できました

あとは、コードをコピペをする場所を整えていきます!

そんなに難しくないので安心してください

スクリプトエディタを開く

コードをコピペする場所であるスクリプトエディタというものを開きます。

  1. 画面上部「ツール」をクリック
  2. 「<> スクリプトエディタ」をクリック
f:id:tabemi:20210523162415p:plain
スクリプトエディタを開く方法

それっぽい画面が出てくればOKです!

ファイルを準備する

今回コードをコピペするには2つの場所が必要となります。
それが、

  • index.html
  • コード.gs

です!

このうち、index.html が存在していないため作成をします。

  1. 「ファイル +」のプラスをクリック
  2. 「HTML」をクリック
  3. 「無題」を「index」とする
f:id:tabemi:20210527222136p:plain:w350
プラスマークをクリックしてHTMLを選択
f:id:tabemi:20210527222140p:plain:w250
index と入力


これで、コピペをする準備の終了です!!


コピペする

では実際にソースコードを貼り付けていきましょう。

今回は、

  • index.js
  • コード.gs

の2つの場所にそれぞれ貼り付けていくので、間違わないようにお気を付けください!


index.html に貼り付け

まずはindex.htmlです。


削除)

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    
  </body>
</html>

既に書いてあるものをすべて削除してください!

そして、下のコードをそのままペーストしてください。

貼付)

<head>
	<base target="_top">


<script type="text/JavaScript">
	google.script.run.withSuccessHandler(main).withFailureHandler(missFunc).getAlldata();
  let limit = 0;
  let i = 0;//開始数
  let end = 0;//処理終了数
  function logger(){
    console.log(`${i} コの処理を開始済み\n${end} コの処理を完了\n残り ${limit-end} コ`);
  }
  function main(json){
    const orders = JSON.parse(json);
    limit = orders.length;
    const timeInterval = 300;
    const timer = setInterval (() =>{
      if( i -end < 25 ){//30でもいいけど、なにかしらほかのプロジェクトのトリガーとかonOpenが起動しないとまずい。
        const order = orders[i]//1つのimgDataを取得
        //GASにデータを渡し、更新スタート!!ルンルン
        google.script.run.withSuccessHandler(makeEyeCatch).withFailureHandler(missFunc).encode(order);
      }
      i++;
      logger();
      if(i===limit) clearInterval(timer);//処理終了
    },timeInterval);
  }

  function makeEyeCatch({row, imageEncode, title}) {
    const board = document.getElementById("board");  //getElementById()等でも可。オブジェクトが取れれば良い。
    const ctx = board.getContext("2d");
    const chara = new Image();
    chara.src = imageEncode;
    
    chara.onload = () => {
      // console.log(chara);
      const imgHeight = chara.naturalHeight;
      const imgWidth = chara.naturalWidth;
      board.height = imgHeight;
      board.width = imgWidth;
      ctx.drawImage(chara, 0, 0);//画像サイズのままcanvasの生成

      /**
       * @type {Array}
       */
      const titles = title.split('\n');
      const moji = new Moji(titles, imgHeight, imgWidth);
      const size = moji.getSize();
      titles.forEach( (text,  i) => {
        const {x, y} = moji.getXYPotision(i);
        ctx.font = size +'px メイリオ';
        ctx.textAlign = "left";
        ctx.fillText(text,x, y);
      });

      const fileName = titles.join('-');
      const imageType = 'image/png';
      const base64 = board.toDataURL(imageType);
      const data = { fileName, imageType, base64, row };
      google.script.run.withSuccessHandler(setState).withFailureHandler(missFunc).saveEyeCatchImg2Folder(data);
    };
  }
  function setState(text){
    const p = document.createElement('p');//テキストで表示する
    p.textContent = "finish: " +text;
    const box = document.getElementById('box');
    box.appendChild( p );//Box要素に入れる
    end++;//処理完了を一つ増やす
    logger();
  }
  
  class Moji {
    constructor(titles, imgHeight, imgWidth) {
      this.titles = titles;
      // const posibleHeight = imgHeight *0.9;//90%の縦幅で文字を配置
      const posibleWidth = imgWidth *0.9;//90%の横幅で文字を配置する

      const getMojiSize = () => {
        // console.log(imgWidth + ":width")
        const maxSize = posibleWidth *0.085;//最大文字サイズ //1200pxなら 1200*0.9*0.08 = 約92px
        const maxMojiLength = this.titles.reduce((number, x) => {
          if (x.length > number) return x.length;
          return number;
        }, 0);
        const size = Math.floor(posibleWidth /maxMojiLength)  //幅から文字数を割って1文字当たりの大きさを出す。
        // console.log(maxMojiLength +"moji => " +size +"px" +", ※max:" +maxSize );
        return maxSize < size ? maxSize: size;//90Px(仮)より大きかったら90px。小さければsize;  
      }
      this.imgWidth = imgWidth;
      this.mojiSize = getMojiSize();
      this.gyo_kan = this.mojiSize *0.4;

      const getInitY = () => {
        const numOfGyo = this.titles.length;
        const mojisHeight = numOfGyo *this.mojiSize;//すべての行の合計高さ
        const gyo_kansHeight = (numOfGyo-1) *this.gyo_kan;//すべての行間の合計高さ
        const mojiBoxesHeight =  mojisHeight +gyo_kansHeight;
        return ( imgHeight -mojiBoxesHeight) /2;
      }

      this.initY = getInitY();
    }
    getSize() {
      return this.mojiSize;
    }
    getXYPotision(i){
      const titleLength = this.titles[i].length;
      const boxWidth = titleLength *this.mojiSize;
      // console.log(boxWidth + "px .boxWidth")
      const x = (this.imgWidth -boxWidth)/2;
      const boxHeight = this.mojiSize +this.gyo_kan;
      const y = (i *boxHeight) +this.initY;
      return {x, y};
    }
    
  }


  function missFunc(){
    const p = document.createElement('p');//完成した料理をテキストで表示する
    p.textContent = 'error';
    p.style.color = 'red';
    const box = document.getElementById('box');
    box.appendChild( p );//Box要素に入れる
  }

</script>
</head>
<div id="box"></div>
<canvas id="board"></canvas>
<canvas id="canvas"> </canvas>
<img id="img" />


すべてもれなくコピペしてください!

コード.gs に貼り付け

続けてコード.gsです。

削除)

function myFunction() {

}


こちらをすべて削除し
下のコードをこちらももれなく貼り付けてください

貼付)

const SHEET_NAME = 'アイキャッチ画像';
const SPREAD_SHEET = SpreadsheetApp.getActiveSpreadsheet();
function initFunction() {
  const sheet = new Sheet(SHEET_NAME);
  const header = [['画像URL', 'アイキャッチタイトル', 'アイキャッチURL']];
  sheet.setValues(header);
  const folderId = DriveApp.createFolder('アイキャッチ画像').getId();
  PropertiesService.getDocumentProperties().setProperty('folderId', folderId);
  ScriptApp.newTrigger("onOpen").forSpreadsheet(SPREAD_SHEET).onOpen().create();//起動時にonOpen関数を実行するトリガーを設置する
}
/**
 * SSにメニューを表示
 */
function onOpen() {
  
  const myMenu = [
    { name: 'アイキャッチ画像生成', functionName: 'openSidebar_' }
  ];
  SPREAD_SHEET.addMenu('自動化ツール', myMenu); //メニューを追加
}

function openSidebar_() {
  const htmlOutput = HtmlService.createTemplateFromFile('index').evaluate().setTitle('ゼロから使える/ZeroScript');
  SpreadsheetApp.getUi().showSidebar(htmlOutput);
}

/**
 * スプレッドシート上の未生成アイキャッチ画像データを返す
 */
function getAlldata() {
  const [header, ...v] = new Sheet(SHEET_NAME).getDataRangeValues();
  if (!v) return [];
  const allData = v.reduce((array, x, i) => {
    if (x[2]) return array;//すでにあれば
    const row = i + 2;//sheetのIndex(starting1)
    const imageUrl = x[0];
    const title = x[1];
    return [ ...array, {row, imageUrl, title} ];
  }, []);
  console.log(allData);
  return JSON.stringify(allData);
}
/**
 * share用Urlからidに変換する
 */
function replaceUrl2id_ (url) {
  const start = url.indexOf('/d/') + 3;
  const end = url.indexOf('/view?usp=sharing');
  return url.substring(start, end);
  // return "https://drive.google.com/uc?id=" +  id;
}
function encode({row, imageUrl, title}){
  const id = replaceUrl2id_(imageUrl);
  var f = DriveApp.getFileById(id);//画像
  var b = f.getBlob();
  var imageEncode = 'data:image/jpeg;base64,' +Utilities.base64Encode(b.getBytes());
  return {row, imageEncode, title};
//  console.log(text);
}
/**
 * @param {blob}
 */
function saveEyeCatchImg2Folder(data) {
  const folderId = PropertiesService.getDocumentProperties().getProperty('folderId');
  const folder = DriveApp.getFolderById(folderId);
  const { fileName, imageType, row } = data;
  const base64 = data["base64"].replace('data:image/png;base64,', '');
  const decoded = Utilities.base64Decode(base64);
  const blob = Utilities.newBlob(decoded, imageType, fileName);
  const url = folder.createFile(blob).getUrl();
  const column = 3;
  new Sheet(SHEET_NAME).setValue(url, row, column);
  return fileName;
}


class Sheet {
  constructor(sheetName) {
    const SS = SpreadsheetApp.getActiveSpreadsheet();
    let sheet = SS.getSheetByName(sheetName);
    if (!sheet) sheet = SS.insertSheet().setName(sheetName);
    this.sheet = sheet;
  }
  /**
   * @return {Array[]}
   */
  getDataRangeValues() {
    return this.sheet.getDataRange().getValues();
  }
  /**
   * @param {Array[]} values
   */
  setValues(values) {
    const row = 1;
    const column = 1;
    const rowNums = values.length;
    const numColumns = values[0].length;
    this.sheet.getRange(row, column, rowNums, numColumns).setValues(values);
  }

  /**
   * @param {string || number} value
   * @param {number} row
   * @param {number} column
   */
  setValue(value, row, column) {
    this.sheet.getRange(row, column).setValue(value);
  }
}


コードのコピペは以上となります。

保存する

コードを削除して貼り付けると、ファイル名のところに赤いポチが付いたのに気が付きましたでしょうか?

これは、未保存ということなので、保存してあげます

  1. Windows=>「Ctrl+s」、Macの方「Comd+s」

これで赤いポチがなくなったかと思います。


▶ボタンを押してみる

  1. コード.gs を開く
  2. ▶ボタンを押す


ペーストして物々しい雰囲気となったエディタ画面の上部に注目してみましょう。

再生ボタンのようなアイコンがあるかと思います。押してみます。

f:id:tabemi:20210523162409p:plain
実行ボタンをクリック

すると、、ウィンドウが出てきますね。。

f:id:tabemi:20210523162329p:plain
ウィンドウが出てくる。「権限を確認」をクリック。キャンセルは押しません。

承認画面を承認する

これは、「あなたに代わってプログラムが色々するけど、大丈夫かい?」という許可をGoogleが求めてきています。許可されない場合、このプログラムは動きませんので、承認しましょう

なにか情報を抜き取るコードなんじゃないの?!

大丈夫です笑

以前私は本気で心配していたんですよね。

だから何度でも言います。大丈夫です。



この認証画面をクリアする手順は、こちらです。

  1. Googleアカウントを選択
  2. 左下「詳細」>「 安全でないページに移動」をクリック
  3. 下にスクロール「許可」をクリック
f:id:tabemi:20210523162340p:plain:w200
Googleアカウントを選択
f:id:tabemi:20210523162346p:plain:w350
詳細をクリック
f:id:tabemi:20210523162354p:plain:w350
焦りますが、「安全ではないページに移動」をクリック
f:id:tabemi:20210523162404p:plain:w350
スクロールして「許可」を選択


そしたら、コードが実行できるようになっているので、
もう一度「再生ボタン」を押してみましょう

エディタ画面
出てくる表示は少し違います。

黄色く「実行完了」が出ていればOKです!
でない、という方は、もう一度▶ボタンを押してみましょう。


以上で、自動生成ができるようになっています!
お疲れ様でした!!

つぎは、使い方を見ていきましょう。

アイキャッチ自動生成ツールの使い方

先ほど作成した、スプレッドシートを見てみましょう。


すると、「アイキャッチ画像」というシートが追加されています。

そして、上部「アドオン ヘルプ」の並びに「自動化ツール」と表示されています。ないよという方、リロードしてみてください。

f:id:tabemi:20210527223946p:plain
「自動化ツール」があればOKです!

使い方概要


使い方の手順をざっと説明すると以下のようになります!

  1. A列にGoogleドライブにアップロードした画像のURLを貼り付ける
  2. B列に画像に入れたい文字を入力する(改行は Ctrl+Enter)
  3. 「自動化ツール」>「アイキャッチ画像生成」をクリック
  4. サイドバー出現
  5. 自動生成開始

Googleドライブへのアップロード方法とUrlの取得方法は、下記にて解説しています。


自動生成されたアイキャッチ画像はGoogleドライブの「アイキャッチ画像」というフォルダの中に保存されています!

f:id:tabemi:20210527223940p:plain
せっまいサイドバーで画像を作っています。。

Googleドライブに画像をアップロードする方法

素材となる画像は、あらかじめGoogleドライブにアップロードする必要があります。

アップロード方法はとっても簡単です。

  1. Googleドライブを開く
  2. 画面左上の 新規+ ボタンをクリック
  3. ファイルをアップロード を選択
  4. アップロードしたいファイルを選択する
f:id:tabemi:20210523162245p:plain:w500
新規+ ボタンをクリック
f:id:tabemi:20210527211853p:plain:w350
ファイルをアップロードを選択します

以上でアップロードができました!

ちなみにアップロードする場所(フォルダ)はどこでも大丈夫ですよ

画像のURLを取得する

スプレッドシートA列には素材となる画像のULRを貼り付ける必要がありました。
そのURLを取得する方法をご紹介します!

  1. アップロードした画像ファイルを右クリック
  2. 「リンクを取得」をクリック
  3. 「リンクをコピー」をクリック
f:id:tabemi:20210527211858p:plain:w400
対象の画像ファイルを右クリックして「リンクを取得」をクリック
f:id:tabemi:20210527211848p:plain:w350
「リンクをコピー」でコピーします

ここで取得したURLをA列に貼り付けましょう!!


解説は以上となります!

最後に

実は、画像に描写して保存するって作業がムリゲー(死語?)だったりするんですが、GASでエンコードして投げることで回避できました。

詳しい方は、例の「汚染」を回避する方法といえば伝わるでしょうか、、

.toDataURL()ができない、
tainted canvases may not be exported です。

私もあまり詳しくはない、いや全然よく分からないのですが、とにかくなんとかなりました。。


というわけで、今日は「6秒でアイキャッチ画像を自動生成する方法」でした。

よいブログライフをお送りください。

以上、たべみでした

ではでは~

【GAS】もう2度と存在しないシートに苦しまない

f:id:tabemi:20210526221034j:plain


どうも、たべみです。

GAS(Google Apps Script)を触っていると存在しないシートを取得しようとして失敗しますよね。

そんな時、シートを生成してくれる関数とクラスを書きました。


何回もエラーが出るから何とかしたい!シートがないときは作ってほしい
そんな方向けの記事です。

const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName/*存在しないシート*/);
const values = sheet.getDataRange().getValues(); // => エラー!!!

解決策その1

では、さっそく。

関数を用いる

function getSheet_(sheetName) {
  const sheet = SS.getSheetByName(sheetName);
  if (!sheet) {//ない場合は
    return SS.insertSheet().setName(sheetName);//シートを挿入する
  } else return sheet;//存在している場合はそのまま返す
}

使い方

function myfuntion() {
  const sheet = getSheet('シート10');
}

うーーん、いい感じです。

解説

const sheet = SS.getSheetByName(sheetName);

シートが存在しなかった場合、getSheetByName()は null を返します
null は false 判定となりますが、 ! (反転演算子) で true となりif文で括弧内の処理に移ります
insertSheet()は新しいシートを作成します。

重要な懸念点

使用すると入力補完が出ません
これがなかなか不便でして、@return を書く方法があるのかと思案しております。要調査です。

function myfuntion() {
  const sheet = getSheet('シート10');
  sheet.  //何も出てこない。 //getDataRange()とか補完してほしい
}

もっと短く書いて懸念点を解決する場合

const sheet = SS.getSheetByName(sheetName) || SS.insertSheet().setName(sheetName);

左辺が null なら右辺が返されるようになっていますが、見にくいですね、、
短く書くのもいいですが、ぱっと見よく分からないのはよくないかもしれないです。

解決策その2

入力補完がないのは不便ですので、ほかの方法をご紹介します!
なかなか使いやすいのではないかと!

クラスを用いる

class Sheet {
  constructor(sheetName) {
    const SS = SpreadsheetApp.getActiveSpreadsheet();
    let sheet = SS.getSheetByName(sheetName);
    if (!sheet) sheet = SS.insertSheet().setName()
    this.sheet = sheet;
  }
  /**
   *シートのすべての値を取得する
   * @return {Array[]}
   */
  getDataRangeValues() {
    return this.sheet.getDataRange().getValues();
  }
  /**
   * 2次元配列をシートに貼り付ける
   * @param {Array[]} values
   */
  setValues(values) {
    const row = 1;
    const column = 1;
    const rowNums = values.length;
    const numColumns = values[0].length;
    this.sheet.getRange(row, column, rowNums, numColumns).setValues(values);
  }
}

使い方

function myfuntion() {
  const sheetName = 'シート11';
  const sheet = new Sheet(sheetName);//インスタンス化
  const values = sheet.getDataRangeValues();
  values.forEach(x => {
    x.forEach(y => console.log(y)) //なんらかの処理
  })
  sheet.setValues(values);
}

クラスなので new を使ってインスタンス化、その後メソッドを呼び出すという形です。
入力補完もつくので良いです!

f:id:tabemi:20210526214109p:plain
インスタンス化してメソッドを呼び出す

解説

シートを扱う関数をすべて class 内のメソッドで行います
値の出し入れ以外に行う場合は、さらにメソッドを作りましょう!

もちろん入力補完が出ますよ。(それしか言わない。)

f:id:tabemi:20210526214105p:plain:w450
入力補完が出た

最後に

シートの取得だけでもの凄く勉強になりますね。ぜひ、こちらをコピペして活用してください!

今回は、シートがないときに作成しつつ取得する関数とクラスを作成してみました!


ではでは~

【限界突破】GASの6分の実行制限の壁を回避する方法

f:id:tabemi:20210525225303j:plain
どうも、たべみです。


業務改善をしているとどうしても気になるGASの実行制限
個人アカウントは6分、その他組織アカウント(有料アカウント)は30分です。


さて、これらを超えないためにAPIの呼び出しを工夫をして減らしたりトリガーを設置したりと対策に苦労している方も多いのではないでしょうか。

私も以前、21万の資料をオーナー権を移譲し、さらにコピーを作成しなければならず、とても制限時間内に行えないかつトリガーを設置しても日が暮れるどころか、季節が変わるのでは。ということがありました。


そこで今回は実行制限を限りなくスルーできるゾンビコードを作成していきます。

解説

今回は、お店を例にしてみました。

f:id:tabemi:20210525214653p:plain:w600
order一覧。3つの料理を一つずつ順番に作った場合、2分15秒かかってしまいます。


SpreadSheet上に3つのオーダーがあり、それぞれA列に調理時間(ミリ秒)、B列に素材、C列に完成品(料理)を出力します。C列は調理後に出力されます。

1つの料理を作成するのにA列の調理時間がかかるとすると、
普通にFor文を回した場合、調理時間がそのまま加算され135000ミリ秒、つまり2分15秒かかってしまうことになります。


どうせなら6分超える時間に設定すればよかったと後悔していますが、、まあ、やっていきましょう笑


早速コードを見てみる

長々と前置き失礼しました。
百聞は一見に如かずです。

コード.gs
/**
 * SSにメニューを表示
 */
function onOpen() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const myMenu = [
    { name: 'サイドバーを表示', functionName: 'openSidebar' }
  ];
  ss.addMenu('調理', myMenu); //メニューを追加
}
function openSidebar() {
  const htmlOutput = HtmlService.createTemplateFromFile('index').evaluate().setTitle('快適');
  SpreadsheetApp.getUi().showSidebar(htmlOutput);
}

/**
 * 全てのオーダーを返す
 */
function getOrders() {
  const [ header,...order] = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0].getDataRange().getValues();
  return JSON.stringify(order);
}

function cooking(json) {
  const { i, cookingTime, material} = JSON.parse(json);
  Utilities.sleep(cookingTime);//調理中....

  const ramen = material +'ラーメン';
  const row = i*1 +2;
  const column = 3;
  SpreadsheetApp.getActiveSpreadsheet().getSheets()[0].getRange(row, column).setValue(ramen);//シートに反映
  return ramen;
}

続いてクライアント側

index.html
<head>
  <base target="_top">
</head>
<div id="box"></div>

<script>
  let orders;
  let stopWatch;
  let waitingClient = 0;//処理の終了を把握する
  // サーバ側のgetOrder関数を呼び出し、成功したらreqestOrder()
  google.script.run.withSuccessHandler(reqestOrder).withFailureHandler(missFunc).getOrders();
  function reqestOrder(json) {
    //グローバルに変数に
    orders = JSON.parse(json);
    //調理開始ボタンを作成
    const button = document.createElement('button');//料理開始ボタンを作成
    button.textContent = '調理開始';
    button.setAttribute('id', 'start-button');
    button.addEventListener('click', startCooking);//クリック時にstartCooking関数を実行
    const box = document.getElementById('box');
    box.appendChild( button );//Box要素に入れる
  }

  function startCooking(){
    stopWatch = new Date();
    document.getElementById('start-button').style.visibility = 'hidden';
    const timeInterval = 1000;//1秒間隔で実行
    let i = 0; 
    const limit = orders.length;
    waitingClient = limit;
    const timer = setInterval (() =>{
      const order = orders[i];//1つの注文を取得
      const [cookingTime, material] = order;//データの中身
      const data = { i, cookingTime, material};//サーバ側に渡す値
      console.log(data)
      //GASにデータを渡し、更新スタート
      google.script.run.withSuccessHandler(setState).withFailureHandler(missFunc).cooking(JSON.stringify(data));
      i++;
      if(i===limit) clearInterval(timer);//処理終了
    },timeInterval);
  }

  function setState(text){
    const p = document.createElement('p');//テキストで表示する
    p.textContent = text;
    const box = document.getElementById('box');
    box.appendChild( p );//Box要素に入れる
    waitingClient--;//調理数を一つ減らす
    if (waitingClient===0){
      const time = new Date()- stopWatch;
      return setState(time +"㎜秒");
    } 
  }

  function missFunc(){
    const p = document.createElement('p');//完成した料理をテキストで表示する
    p.textContent = 'error';
    p.style.color = 'red';
    const box = document.getElementById('box');
    box.appendChild( p );//Box要素に入れる
  }
</script>

全体の流れ

前提として、onOpenはスプレッドシート起動時に実行するトリガーが設置されています。

  1. スプレッドシート上部ツールバーからサイドバーを表示する
  2. サイドバー(index.html)が表示され、クライアント側からGAS側のgetOrders関数が呼び出される
  3. クライアント側は、戻り値である2次元配列(オーダー一覧)を受け取る。
  4. クライアント側から配列を一つずつ取り出し、1秒ごとにGAS側へ送る(cooking関数)
  5. GAS側は受け取ったデータを処理する(cooking関数)

GASの関数実行は4回に分かれています。getOrders()とオーダーの数だけの(今回は3つ)cooking()となります。

そして肝心なことは、関数を同時に実行している点にあります。つまりcooking関数を直列ではなく、並列に処理をしていることになります。

GASのみに注目するとこうなります。

  • getOrders()はクライアント側にオーダー一覧を返して終了
  • cooking()は一つのオーダーを調理して終了
  • cooking()は1秒毎にオーダーが飛ばされる

応用するときのイメージは以下です。

  1. GASから処理全体のデータをクライアント側に投げる
  2. クライアント側から処理の一つずつをGASに投げる
f:id:tabemi:20210525214648p:plain:w500
全処理が終了。1分4秒ほどで終了しています。

google.script.runが非同期に処理を実行

クライアント側からGAS側のコードを呼び出すには、 google.script.run を使います。Webアプリを作るときによく使用する関数ですね。

使い方

google.script.run.withSuccessHandler( 正常に動いたときに実行する関数 ).withFailureHandler(エラーがおこったときに実行する関数 ).GAS側の関数(引数);

注意点

いくつか実装に当たって注意があります。

とはいっても実行時間制限

並列処理をしていても、一つの処理を実行するのに6分以上かかってはいけません
今回の例では、cooking関数(ひとつの調理時間)が6分を超えた時点で「実行時間が長すぎます」と例のエラーが発生してしまいます。
一つの関数に命を捧げましょう。

実行数制限

いくらでもGASを呼び出して時間を短縮してやる。と思いがちですが(私のこと)、GASには同時実行数にも制限がかかっており、無料・有料アカウントともに同時実行可能数が30までとなっています。
そのため、クライアント側で30を超えないように制御してください!

サーバーに負荷

timeIntervalは余裕をもって設定してください!

今回のような処理であれば、あまり気にすることはないですが、スプレッドシートを新しく作成したり、シートを挿入したりというような処理は、同時にできないことがあります。

といっても経験則でしかないので、、具体的にどれということは難しいですが、
サーバーに負荷が大きそうな処理同時実行の間隔を空けてみてください
明確な根拠をもって回答ができなくて、申し訳ありません。。

小話

htmlファイルにタグとタグを書いておりません。これはslideshareに記載されていたのを参考にしています。
現在、絶賛捜索中なのですが、見つかりません💦
Google勤務の方が書いていたように思います。。

信憑性のかけらもないお話でした、、、(かならず見つけ出す!!)

最後に

なんだか、歯切れの悪い最後になってしまったのですが、今回は以上となります。

クライアント側からサーバ側の処理を管理できるため、処理過程を記述することもできますよ

不明点ありましたら、コメントください!

今回は、GASの実行時間制限を回避する方法をお伝えしました。

ではでは~

【コピペ】ゼロからわかる公式LINEに電子メニューBOTを作る方法_後編

f:id:tabemi:20210524130809j:plain
どうも、たべみです。
前回に引き続き、公式LINEのトークルームに電子メニューを作成する方法を、前知識ゼロでも分かるよう解説いたします!

前回の記事はコチラ
tabemi.hatenablog.com

3.公式LINEと紐づける

前回は、Googleスプレッドシートを用意し、コードをコピー&ペースとするところまで解説いたしました。続いて、公式LINE上での操作となります!

3-1.公式LINEのホームに移動する

ここをクリックして、公式LINEアカウントマネージャーに移動しましょう。
アカウント持ってないよ!という方は、下のLINEの公式ページを参考にアカウントを作成してください。
www.linebiz.com

LINEアカウントマネージャーのホーム画面の画像
LINEアカウントマネージャーのホーム画面

3-2.応答モードをBotに変更する

プログラムでチャットが応答するモードBot)に変更しましょう。

  1. 画面上部の「応答モード:チャット」の部分をクリック
  2. 基本設定の応答モードを「Bot」に変更
  3. 詳細設定のWebhookを「オン」に変更

詳細設定の応答メッセージは「オン」でも可ですが、すべてのメッセージに反応するdefaultのメッセージ設定は削除しておきましょう。

LINEアカウントマネージャーのホーム画面の画像
ホーム画面より「応答モード」をクリック
f:id:tabemi:20210524112427p:plain
応答モードを「Bot」に変更
f:id:tabemi:20210524112433p:plain
Webhookを「オン」に変更。応答メッセージはオンでも可ですが、すべてのメッセージに反応するdefaultのメッセージは削除しておきましょう。

3-3.チャンネルのアクセストークンを取得する

ここからが実際に紐づけていく作業となります!あと少しですので頑張りましょう!!

ここをクリックし、LINEの開発者向けのページであるコンソールを開きましょう。

f:id:tabemi:20210524112355p:plain:w350
リンク遷移先の画面。設定したいチャンネルを選びましょう。


この画面からアクセストークというものを取得していきます。アクセストークンとは簡単に言って、外部から公式LINEにアクセスできる鍵のようなものです。
そのため、アクセストークンは基本的に自分以外の人間に公開しないように気を付けて取り扱ってください!

それではアクセストークンの取得方法です。

  1. Message API設定」をクリック
  2. 一番下までスクロールし「チャンネルアクセストークン(長期)」の「発行」ボタンをクリック
  3. 文字の羅列が表示されるのでコピーする
f:id:tabemi:20210524112339p:plain
チャンネルのホーム画面。Message API設定をクリック
f:id:tabemi:20210524112345p:plain
アクセストークンを発行する
f:id:tabemi:20210524112350p:plain
発行後の画面。紙のマークをクリックするとコピーができます。

3-4.アクセストークンをGoogleスプレッドシートのプログラムに設定する

作成したプログラムが公式LINEにアクセスできるように、アクセストークンを設定していきます。
手順2で開いていたエディタ画面を開きましょう。

Google Apps Script エディタ画面
エディタ画面

コピペしたコードにアクセストークンを設置します。ここではわかりやすさのため、アクセストークンが 1234567890 であったと仮定してご説明します。

  1. コピペしたコードの一番上の【ここを削除】を削除。
const ACCESS_TOKEN = "【ここを削除】"

【】も削除してください。

const ACCESS_TOKEN = ""

ダブルクォーテーションが2つ残ります。

  1. ダブルクォーテーションの間にアクセストークンの 1234567890 をペースト
const ACCESS_TOKEN = "1234567890"

となるはずです。

以上でアクセストークンの設置完了です!!

Google Apps Script エディタ画面
実際のアクセストークンを貼り付けたときの画像。アクセストークンが長いため、2行になるかもしれません。

3-5.プログラムのURLを発行する

そのままエディタ画面を開いたままにして、今度は、
プログラムが公式LINEにメッセージを送るためのURLを発行します。

  1. エディタ画面の右上、「デプロイ」をクリック
  2. 「新しいデプロイ」を選択
  3. 種類の選択の歯車アイコンをクリック
  4. ページ下方、アクセスできるユーザーを「全員」にする
  5. 「デプロイ」
  6. 「ウェブアプリ」の下にある「https://」で始まるURLをコピー

こちらのURLもアクセストークンと同様に、自分以外にむやみに公開しないようにしましょう!

エディタ画面
「デプロイ」>「新しいデプロイ」とクリックする
エディタ画面
歯車アイコンをクリックし、「ウェブアプリ」を選択
エディタ画面
一番下の「アクセスできるユーザー」を「全員」にして「デプロイ」
デプロイ画面
デプロイ完了後の画面。URLをコピーしておく。

3-6.発行したURLを公式LINEに設定する

手順3の最後です!
先ほど発行したURLを公式LINEに設定をして、メッセージが送信されるたびにプログラムが呼び出されるようにします。

先ほどアクセストークンを取得したページに移動します。
LINEの開発者向けのページであるコンソール

  1. 「Message API設定」の「Webhook URL」に発行したURLを設定します。
  2. Webhookの利用」をオンにします。

以上でメッセージ送信されるとプログラムが起動するようになりました!

Webhook設定後の画像
Webhook設定後

4.メニューを埋めて実機テスト!

お疲れ様です!設定はすべて完了です。
ここからは、スプレッドシートメニュー表の使い方について解説していきます。

4-1.メニューの設定方法

手順1で作成したスプレッドシートを開いてみましょう
画面下部に注目すると、「店舗情報」、「カテゴリ」、「メニュー」シートが追加されていることが分かります。
そして、画面上部の、「アドオン ツール ヘルプ」の並びに <登録> ボタンが追加されています。

おおまかなメニュー設定の手順

  1. メニュー表を埋める
  2. <登録>ボタンを押し、「更新を登録」をクリック
f:id:tabemi:20210524123440p:plain:w500
スプレッドシートの上部に 登録 ボタンが追加されています

以上で設定したメニューが、公式LINE上に反映されます
試しに、公式LINE上で「メニュー」と送信してみましょう。デフォルトで「ハンバーガー」カテゴリを設定したので、メニューを編集していなければハンバーガー」のタブが現れたかと思います!


4-2.メニュー表の埋め方

あとは、自分でメニューをカスタマイズしていくだけです!

シート名 編集箇所 制限
店舗情報 A2セルのみ編集 特になし
カテゴリ A2:A14 カテゴリは13コまで
メニュー A2:D131 各カテゴリ10個まで。カテゴリ別に並べなくて大丈夫です

メニューシート詳細

注意
A カテゴリ名と同じ名称にしてください
C 半角数字のみ

以上が編集する際の注意となります!
最後に「更新を登録」ボタンを押し忘れないようにだけご注意ください。

f:id:tabemi:20210524123428p:plain:w500
店舗情報はA2セルに記入。セル内での改行は、「Ctrl+Enter」です。
f:id:tabemi:20210524123428p:plain:w500
カテゴリシートの記入例。13個までしか登録できません
f:id:tabemi:20210524123434p:plain:w500
メニューシートの記入例。価格は半角数字で。空欄含まないように注意です。

最後に

ここまで、公式LINE上に電子メニューを作成する方法を解説してきました。
長い作業でしたが、最後までお読みいただきましてありがとうございました!

画像など付けたい!という方には、ご相談受け付けております。
公式LINEに非接触メニューBOTを作成します 【更新・維持費0円】飲食店応援!安心・楽しい食事をサポート

少しでも皆さんのお力になれたらと思っています。
不明点などありましたら、コメントにて承っていますので、お気軽にご相談ください!


次回は、すこしでも業務を自動化してみたいと考えている方に、ここでコピペしたコードについて解説していきたいと思います。

今回はこの辺で失礼します。


ではでは~

【コピペ】ゼロからわかる公式LINEに電子メニューBOTを作る方法_前編

アイキャッチ画像
どうも、たべみです。
今日は、公式LINEのトークルームに電子メニューを作成する方法を、前知識ゼロでも分かるよう解説いたします!


公式LINE始めたけど、トークルームを活用できてない、、、そんな方にオススメですよ。
ちなみに、これから紹介する内容は料金かかりません!幸せ。

完成イメージ

「メニュー」と送信すると、メニューのカテゴリが現れ、
それをタップするとそのカテゴリのメニューが表示されます。
「店舗情報」と送信すると、店舗情報が表示されます。

これらはGoogleスプレッドシートを操作して内容を更新することができる仕様になっています。

f:id:tabemi:20210523191252p:plain:w250
メニューと送信するとメニューカテゴリを選択できるようになります。
f:id:tabemi:20210523191257p:plain:w250
ハンバーガーを選択するとハンバーガーのメニューが現れます。
Googleスプレッドシートの画像
Googleスプレッドシート。ここからカテゴリを設定できる。
Googleスプレッドシートの画像
このシートからカテゴリ別にメニューが送信される。いつでも更新ができる。


サンプルを公開していますので、ご覧ください。
lin.ee


おおまかな4ステップ

それでは、作業を開始していきます!
まずは全体の流れを把握しておきましょう。

1.Googleスプレッドシートを用意してする

Googleスプレッドシートの画像
Googleスプレッドシート

Googleスプレッドシートという、Excelのような表アプリにメニュー表を作成していきます。

ここの手順では行いませんが、メニュー表の記入には場所やルールがありますので、それに従って埋めていくことになります。
とは言っても、全く難しくないのでご安心ください!!

2.Google Apps Scriptをコピペする

Google Apps Scriptエディタの画像
Google Apps Scriptエディタ

プログラミングなんてできないよ。。という方でも大丈夫なように、完全コピペ、、、とまではいきませんが、9割5分コピペで大丈夫な作業です。

コピペってなんか情報抜き取るヤバいコードが書いてあるんじゃないの??って不安な方(以前の私。)、大丈夫ですよ(笑)
「プログラミング学習してみたい!」という方、後日コードの解説をしていきます~!

3.公式LINEと紐づける

こちらも天下のLINEさんが素晴らしく分かりやすい設定画面を準備して下さっている(皮肉とかではなくほんとに。)ので、私の拙文でもわかりやすく解説いたします。

すでに公式LINEを持っていらっしゃる方は、なお理解しやすいかもしれませんね!

4.メニューを埋めて実機テスト!

あとは実際にメニュー表(Googleスプレッドシート)を埋めて、公式LINEで動作を確認してみましょう!
ここまでのすべての作業時間は1時間かかるかな?くらいだと思います。
晴れて完成ですよ!

前編では1と2の手順について解説します。

1.Googleスプレッドシートを用意してする

Googleスプレッドシート
Googleスプレッドシート

それでは、実際に作業を始めていきましょう!!

1-1.Googleにログインする

ここにアクセスしてみましょう。Googleドライブにアクセス出来たら成功です。
ログイン画面が出てきた方はログインしましょう。

Googleログイン画面
ログイン画面が出てきたらログインしましょう

Googleアカウント持ってないよ!という方は、「アカウントを作成」をクリックして新しく作成しましょう。
作り方は、こちら↓です
support.google.com
また、こちらの解説が大変分かりやすいので参考にされると良いかもしれません。
Googleアカウントの作成方法を丁寧に全解説【PC・スマホ】 | アクセス中古ドメイン
ちなみに、名前は本名でなくて大丈夫ですよ。

1-2.新しいGoogleスプレッドシートを作成する

ログイン後の画面がこちら。

Googleドライブの画像
Googleドライブ

画面左上の「+新規」をクリックし、「Googleスプレッドシート」を選択して下さい。

新規ファイルの作成の画像
新規ファイルの作成
f:id:tabemi:20210523162249p:plain:w200
スプレッドシートを選択する

これでGoogleスプレッドシートが作成できました。いやん、簡単。涙出てくる。

Googleスプレッドシートの画像
Googleスプレッドシート

「無題のスプレッドシート」とタイトルがついているかと思われますので、お好きなタイトルに変更すると良きかもしれないですよ。

2.Google Apps Scriptをコピペする

Google Apps Scriptのエディタ画面の画像
Google Apps Scriptのエディタ画面

プログラムをコピー&ペーストしていきます。なんども言いますが、情報を抜き取ったり、危ない感じになったり?、そんなことはありませんのでどうかご安心を。。。

2-1.コピペする場所(エディタ)に移動する

さて、先ほど作成したGoogleスプレッドを開いてください。開いてるよって?

そしたら、上部の「ツール」をクリックし、上から3番目の「〈〉スクリプト エディタ」を開いてください。すると別タブにそれっぽい画面が表示されると思います。

f:id:tabemi:20210523162415p:plain
「ツール」を選択し、「<> スクリプト エディタ」をクリック

あれ、全然動かなくなちゃったんだけど、って方は、一旦スプレッドシートとそのエディタもタブを消して、もう一度Googleドライブから入りなおして同じ作業を繰り返してみてください。

2-2.myFunction(){ }を削除する

f:id:tabemi:20210523162235p:plain
スクリプトエディタ

それっぽい画面の中央付近に「myFunction(){ (改行) }」とありますね。これをすべて消してください。普通に「BackSpace」キーで削除できます。

f:id:tabemi:20210523162240p:plain
削除した後のエディタ

2-3.コードをコピペする

「myFunction(){ (改行) }」のあった部分に、下のコードをコピーしてペースト(貼り付け)てみましょう。コピーは「Ctrl+c」で、ペーストは「Ctrl+v」でできます。右クリックしてもできますよ。

const ACCESS_TOKEN = "【ここを削除】"// => 【】も削除!
const SPREAD_SHEET = SpreadsheetApp.getActiveSpreadsheet();

//スプレッドシートを所定の書式に変更する
//
function initFunction() {
  //新しいシートを作成する関数
  const createSheets = ({ sheetName, values }) => {
    try {
      const sheet = SPREAD_SHEET.insertSheet(sheetName);//新しいシートを作成する
      console.log(sheetName + "を作成しました。");
      if (!values) {//valuesがなけ(データ取得用のシートであ)れば、非表示にするシート
        sheet.hideSheet();
      } else {//編集用のシートであれば
        const row = values.length;
        const column = values[0].length;
        sheet.getRange(1, 1, row, column).setValues(values);//新しいシートに値を貼り付ける
      }
    } catch (e) {
      console.log(sheetName + "の作成に失敗しました。");
      console.error(e);
    }
  }

  //新しく作成するシートの情報
  ////編集用のシート=>日本語のシート, データ取得用のシート=>ローマ字のシート
  const sheetsInfo = [
    {
      sheetName: "店舗情報",
      values: [["店舗情報"],[ "店舗名:\n店舗住所:\n店舗SNS:"]],
    }, {
      sheetName: "カテゴリ",
      values: [["カテゴリ名"], ["ハンバーガー"]],
    }, {
      sheetName: "メニュー",
      values: [["カテゴリ", "メニュー名", "価格", "説明"], ["ハンバーガー", "バーベキューハンバーガー", 1000, "おいしいよ。"]],
    }, {
      sheetName: "info"
    }, {
      sheetName: "category"
    }, {
      sheetName: "menu"
    }
  ];

  sheetsInfo.forEach(createSheets);//新しいシートを作成する処理を行う
  ScriptApp.newTrigger("onOpen").forSpreadsheet(SPREAD_SHEET).onOpen().create();//起動時にonOpen関数を実行するトリガーを設置する
}


function onOpen() {
  const myMenu = [
    { name: '更新を登録', functionName: 'updateMenu' }
  ];
  SPREAD_SHEET.addMenu('<登録>', myMenu);
}

//編集用のシート=>fromSheet, データ取得用のシート=>toSheet
class Sheet {
  constructor(sheetName) {
    const sheetNumsObj = {
      "category": {
        fromSheetName: "カテゴリ",
        numRows: 13,
        numColumns: 1
      },
      "menu": {
        fromSheetName: "メニュー",
        numRows: 10,
        numColumns: 4
      },
      "info": {
        fromSheetName: "店舗情報",
        numRows: 2,
        numColumns: 1
      }
    };
    const sheetNums = sheetNumsObj[sheetName];
    this.toRange = SPREAD_SHEET.getSheetByName(sheetName)
      .getRange(1, 1, sheetNums["numRows"], sheetNums["numColumns"]);
    this.numRows = sheetNums["numRows"];
    this.numColumns = sheetNums["numColumns"];
    this.fromSheetName = sheetNums["fromSheetName"];
  }
  getValues() {
    return this.toRange.getValues();
  }
  peastValues() {
    const fromValues = SPREAD_SHEET.getSheetByName(this.fromSheetName)
      .getRange(2, 1, this.numRows, this.numColumns)
      .getValues();//編集シートの値を取得
    this.toRange.setValues(fromValues);
  }
}

function updateMenu() {
  const toSheets = ["category", "menu", "info"];
  toSheets.forEach(sheetName => {
    const sheet = new Sheet(sheetName);
    sheet.peastValues();
  });
}



//LINEから呼び出される。
function doPost(e) {
  // WebHookで受信した応答用Token
  const event = JSON.parse(e.postData.contents).events[0];
  const url = 'https://api.line.me/v2/bot/message/reply';
  const payload = getPayload(event);
  const headers = {
    'Content-Type': 'application/json; charset=UTF-8',
    'Authorization': 'Bearer ' + ACCESS_TOKEN
  }
  const options = {
    'headers': headers, 'method': 'post',
    'payload': JSON.stringify(payload),
    // muteHttpExceptions: true, // エラーをデバックする場合、コメントオフ
  };
  try {
    UrlFetchApp.fetch(url, options);//LINEに情報を流す
  } catch (e) {
    console.error(e);
  }
  return ContentService
    .createTextOutput(JSON.stringify({ 'content': 'post ok' }))
    .setMimeType(ContentService.MimeType.JSON);
}

function getPayload(event) {
  const replyToken = event["replyToken"];//LINEに送信するToken
  const type = event["type"];
  console.log("type:" +type)
  //受け取ったメッセージタイプがmessageであれば
  if (type === "message") {
    const text = event["message"]["text"];
    if (text === "メニュー") {//テキスト内容が"メニュー"で
      return {
        'replyToken': replyToken,
        "messages": makeCategoryMessages()
      };
    } else if (text === "店舗情報") {
      return {
        'replyToken': replyToken,
        "messages": makeInfoMessages()
      };
    }

    //受け取ったメッセージタイプがpostboackイベント(カテゴリをタップした場合にpostbackになる)であったら
  } else if (type === "postback") {
    const categoryName = event["postback"]["data"];
    return {
      'replyToken': replyToken,
      "messages": makeMenuMessages(categoryName)
    };
  }
}


//category を返す関数
function makeCategoryMessages() {
  const createContent = (categoryName) => {
    const action = {
      "type": "postback",
      "label": categoryName,
      "data": categoryName
    }
    return {
      "type": "action",
      "action": action
    }
  };

  const categorySheet = new Sheet("category");
  const values = categorySheet.getValues();//[[categoryName0],[categoryName1],...];
  const items = values.reduce((array, x) => {
    const categoryName = x[0];
    if (!categoryName) return array;//categoryNameが空欄であれば追加しない。
    return [...array, createContent(categoryName)];
  }, []);
  console.log(items);
  if (!items.length) return [{ "type": "text", "text": "メニューがありません" }];

  return [{
    "type": "text",
    "text": "メニューカテゴリです",
    "quickReply": { "items": items }
  }];
}


//menuを返す関数
function makeMenuMessages(categoryName) {
  console.log(categoryName);
  const numberWithDelimiter = (number) => {
    return "¥" + String(number).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,');
  }
  const createContent = ([menuName, price, discription]) => {
    return {
      "type": "bubble",
      "header": {
        "type": "box",
        "layout": "vertical",
        "contents": [],
        "backgroundColor": "#666666"
      },
      "body": {
        "type": "box",
        "layout": "vertical",
        "contents": [
          {
            "type": "text",
            "text": menuName,
            "size": "xl"
          },
          {
            "type": "text",
            "text": numberWithDelimiter(price),
            "style": "italic",
            "align": "end",
            "size": "lg"
          },
          {
            "type": "text",
            "text": discription,
            "wrap": true,
            "color": "#666666",
            "margin": "lg"
          }
        ]
      }
    }

  }

  const menuSheet = new Sheet("menu");
  //[["メニュー名", "価格", "説明"],[],...]
  const values = menuSheet
    .getValues()
    .reduce((array, x) => {
      if (x[0] !== categoryName || !x[1]) return array;//カテゴリ名が同じメニューのみ取り出す。
      return [...array, [x[1], x[2], x[3]]];
    }, []);

  if (values === []) return [{ "type": "text", "text": "メニューがありません" }];
  const contents = values.reduce((array,x) => [ ...array, createContent(x)] ,[]);
  console.log(contents);
  return [{
    type: "flex",
    altText: categoryName + "メニューです",
    contents: {
      type: "carousel",
      contents: contents
    }
  }];
}

function makeInfoMessages() {
  const infoSheet = new Sheet("info");
  const infoText = infoSheet.getValues()[0][0];//B1セルを取得
  console.log(infoText)
  return [{ "type": "text", "text": infoText }];
}

過不足なく、全て貼り付けてください。

そして、「Ctrl+s」で保存します。マックの方は、「Comand+s」です。

f:id:tabemi:20210523162319p:plain
貼り付け後はこんな感じになります

これ以降の作業でエラーが出る場合正確に張り付けられていないことが原因です!

2-4.▶ボタンを押してみる

ペーストして物々しい雰囲気となったエディタ画面の上部に注目してみましょう。再生ボタンのようなアイコンがあるかと思います。押してみましょう。

f:id:tabemi:20210523162409p:plain
実行ボタンをクリック

すると、、ウィンドウが出てきますね。。

f:id:tabemi:20210523162329p:plain
ウィンドウが出てくる。「権限を確認」をクリック。キャンセルは押しません。

2-5.承認画面を承認する

これは、「あなたに代わってプログラムが色々するけど、大丈夫かい?」という許可をGoogleが求めてきています。許可されない場合、このプログラムは動きませんので、承認しましょう

手順は、

  1. Googleアカウントを選択
  2. 左下「詳細」>「 安全でないページに移動」をクリック
  3. 下にスクロール「許可」をクリック

です。

f:id:tabemi:20210523162340p:plain:w200
Googleアカウントを選択
f:id:tabemi:20210523162346p:plain:w200
詳細をクリック
f:id:tabemi:20210523162354p:plain:w200
焦りますが、「安全ではないページに移動」をクリック
f:id:tabemi:20210523162404p:plain:w200
スクロールして「許可」を選択

こちらのプログラムは、、、

以外、行っていません!ご安心を!とくに1年前くらいの私!

そしたら、コードが実行できるようになっているので、もう一度「再生ボタン」を押してみましょう

エディタ画面
もう一度、実行ボタンを押すと、下から画面が出てきて、成功したことが分かる

「実行完了」が出ていればOKです!

最後に

今回は、公式LINEに電子メニューBOTを作る方法の前編として、Googleスプレッドシートの作成、Google Apps Scriptのコピペを解説しました。不明点あれば、ぜひコメントでお知らせください
また、Googleアプリでこんなこと自動化できない?Google Apps Scriptで書いてみたいこと、分からないことなど、お気軽にコメント欄でお知らせください!

それでは、次回は公式LINEの設定とメニュー表を埋めてLINEでの動作を確認してみましょう!

ではでは~