【Node.js, TypeScript】現在地から美味しいお店を探すアプリをLINE Messaging APIで作ってみた！

以前LINE Messaging APIを使って、天気予報アプリを作成しました。

https://qiita.com/Ryo9597/items/bcb807688f35d6a72861

https://qiita.com/Ryo9597/items/bf93618663f28e9d8f7f

https://qiita.com/Ryo9597/items/4c05b8b972ad7efc0c66

今まで作った記事の中で一番いいねを頂きました☺️

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/350ecb09-9df5-bf70-75d0-a7a013928772.png)

ということで、第2弾です。

LINE Messaging APIを使って現在地から美味しいお店を探すアプリを作ってみました。
完成形としては以下の通りです。

![名称未設定](https://user-images.githubusercontent.com/70458379/123104385-dc754d00-d471-11eb-8baa-7abdfd400caf.png)

![名称未設定2](https://user-images.githubusercontent.com/70458379/123104553-03cc1a00-d472-11eb-805b-5b52085a428a.png)

## どのようなアプリか

皆さんは、どのようにして飲食店を探しますか？
私は、食べログなどのグルメサイトを使わずに Google Mapで探します。

以前食べログで「星 3.8 問題」がありました。

https://news.yahoo.co.jp/byline/toryu/20191014-00146785/

これだけではなく、食べログで見つけた行ったお店がイマイチだったこともあり、
グルメサイトはお店を探す場所ではなく、お店を予約するためのサイトと私は割り切りました。
**電話が苦手な自分としては、まだまだ飲食店で独自の予約サイトを持っている企業も少ないので、食べログやホットペッパーで予約が可能なのはすごく助かっています。**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/85f85fb4-7cc4-9d27-93a7-0467b9778fc4.png)

Google Mapでお店を探すのもなかなか手間がかかるので、今回はGoogle Mapを使って近くの名店を10個教えてくれるアプリを作成しました。

## アプリの流れ

アプリの流れは大まかに以下の9つのステップで成り立っています。

| クライアント            | LINE（バックエンド）                                                |
| ----------------------- | ------------------------------------------------------------------- |
| ①「お店を探す」をタップ |                                                                     |
|                         | ②「現在地を送る」ためのボタンメッセージを送信                       |
|                         | ③「お店を探す」以外のメッセージが届いたときにエラーメッセージを送信 |
| ④ 現在地を送る          |                                                                     |
|                         | ⑤ お店の配列を作成する                                              |
|                         | ⑥ 必要なデータのみにする                                            |
|                         | ⑦ 評価順に並び替えて上位 10 店舗にする                              |
|                         | ⑧ Flex Message を作成する                                            |
|                         | ⑨ お店の情報を Flex Message で送る                                  |

## Github
完成形のコードは以下となります。

https://github.com/ssk9597/LINE-Node-TypeScript-Gourmet

## ディレクトリ構造
先にざっくりと構造を説明します。
`api/controller/src`内にコードを書いていきます。
コンパイルを行うと、それらのコードが`api/controller/dist`に格納されます。

また、`src/Common`に関数を切り分けています。
`TemplateMessage`はメッセージの雛形を作るまでの過程を全てこちらのファイル内で行っています。
`SendMessage`はメッセージを送信する関数があります。

```
.
├── api/
│   ├── controller/
│   │        └── dist/ (コンパイル後)
│   │        └── src/ (コンパイル前)
│   │        │        └── Common/
│   │        │        │        └── SendMessage/ (メッセージを送信する)
│   │        │        │        └── TemplateMessage/ (メッセージのテンプレートを作成する)
│   │        │        └── index.ts
├── node_modules
├── .env
├── .env.example
├── .gitignore
├── package.json
├── package-lock.json
├── README.md
├── tsconfig.json
```

## では実際に作成していきましょう！

### LINE Developersにアカウントを作成する

[LINE Developers](https://developers.line.biz/ja/)にアクセスして、「ログイン」ボタンをクリックしてください。

その後諸々入力してもらったら以下のように作成できるかと思います。
注意事項としては、今回Messaging APIとなるので、チャネルの種類を間違えた方は修正してください。

![スクリーンショット 2021-06-24 9.16.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/f6a6ca01-5356-a80b-8fee-5ab94a98fb14.png)

**チャネルシークレット**と**チャネルアクセストークン**が必要になるのでこの2つを発行します。

![スクリーンショット 2021-06-24 9.18.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/4176fdc1-4a08-36e5-3ffe-15d0147cf305.png)

![スクリーンショット 2021-06-24 9.19.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/91c2ccd9-aa83-f406-0dc8-b6c83da0173f.png)

ではこの2つを`.env`に入力します。

```env:.env
# LINE（ご自分のBotの値に書き換えを行ってください）
CHANNEL_SECRET = "a9152"
CHANNEL_ACCESS_TOKEN = "zTon3"
```

### `package.json`の作成
以下のコマンドを入力してください。
これで、**package.json**の作成が完了します。

```terminal:ターミナル
$ npm init -y
```

### 必要なパッケージのインストール
#### dependencies
dependenciesはすべてのステージで使用するパッケージです。

今回使用するパッケージは以下の4つです。

| 必要なパッケージ |
| ---------------- |
| @line/bot-sdk    |
| express          |
| dotenv           |
| axios            |

以下のコマンドを入力してください。
これで全てのパッケージがインストールされます。

```terminal:ターミナル
$ npm install @line/bot-sdk express dotenv axios --save
```

#### devDependencies
devDependenciesはコーディングステージのみで使用するパッケージです。

今回使用するパッケージは以下の7つです。

| 必要なパッケージ |
| ---------------- |
| typescript       |
| @types/node      |
| @types/express   |
| ts-node          |
| ts-node-dev      |
| rimraf           |
| npm-run-all      |


以下のコマンドを入力してください。
これで全てのパッケージがインストールされます。

```terminal:ターミナル
$ npm install -D typescript @types/node @types/express ts-node ts-node-dev rimraf npm-run-all
```

### `package.json`にコマンドの設定を行う
**`npm run dev`**が開発環境の立ち上げに使います。
**`npm run start`**が本番環境用です。

```json:package.json
{
  "scripts": {
    "dev": "ts-node-dev --respawn api/controller/src/index.ts",
    "clean": "rimraf dist",
    "tsc": "tsc",
    "build": "npm-run-all clean tsc",
    "start": "npm run build && node ."
  },
}
```

### tsconfig.jsonの作成
以下のコマンドを実行しTypeScriptの初期設定を行います。

```terminal:ターミナル
$ npx tsc --init
```

それでは、作成された**tsconfig.json**の上書きをしていきます。

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "./api/controller/dist",
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["api/controller/src/**/**/*"]
}
```

簡単にまとめると、
**`api/controller/src`**ディレクトリ以下を対象として、それらを**`api/controller/dist`**ディレクトリにES6の書き方でビルドされるという設定です。

tsconfig.jsonに関して詳しく知りたい方は以下のサイトをどうぞ。

https://book.yyts.org/handson/tsconfig.json-settings

また、この辺りで必要ないディレクトリはGithubにpushしたくないので、**`.gitignore`**も作成しておきましょう。

```.gitignore:.gitignore
node_modules
package-lock.json
.env
dist
```

### `https://localhost:3000`にアクセスすると`hello world`が表示
APIサーバーが正しく動くか検証のため一応作っておきましょう。

```typescript:api/controller/src/index.ts
// Load the package
import { Client, ClientConfig } from '@line/bot-sdk';
import express from 'express';
import dotenv from 'dotenv';
dotenv.config();

// Read the ports from process.env.file
const PORT = process.env.PORT || 3000;

// Load the access token and channel secret from the .env file
const clientConfig: ClientConfig = {
  channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '',
  channelSecret: process.env.CHANNEL_SECRET || '',
};

// Instantiate
const app: express.Express = express();
const client: Client = new Client(clientConfig);

// Do routing
// Test
app.get('/', (req: express.Request, res: express.Response): void => {
  res.send('Hello World');
});

// Start the server
app.listen(PORT, (): void => {
  console.log('http://localhost:3000');
});
```

上記の内容はほぼ公式サイトからコピペしているだけなので特に難しいところはないと思います。
公式サイトを載せておきますのでこちらをこちらを見て下さい。

https://line.github.io/line-bot-sdk-nodejs/getting-started/basic-usage.html#basic-usage

https://line.github.io/line-bot-sdk-nodejs/guide/typescript.html#typescript

https://github.com/line/line-bot-sdk-nodejs/blob/master/examples/echo-bot-ts/index.ts

### `localhost.run`で開発用のhttpsを取得

以前作成した[天気予報アプリ](https://qiita.com/Ryo9597/items/4c05b8b972ad7efc0c66)の記事でも使った**`localhost.run`**を使います。

http://localhost.run

ということでここからはターミナル2つ使って開発していきます。
こんな感じです。

![スクリーンショット 2021-06-24 10.04.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/ce93d1cd-cb9b-fb17-654d-93d78fd796d0.png)

ターミナルに2つのコードを貼り付けて実行してください。

```terminal:ターミナル
$ npm run dev
$ ssh -R 80:localhost:3000 localhost.run
```

Webhook URLの登録
localhost.runで作成したhttpsのURLをコピーしてください。
私の場合は以下のURLです。

https://6a4ce98ffe70d5.localhost.run

これをLINE DevelopersのWebhookに設定します。

![スクリーンショット 2021-06-24 10.07.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/23ed875b-1365-9be3-c598-30e39cef608f.png)

これで初期設定は完了です。

ここまでの作業は以下のブランチで行っています。

https://github.com/ssk9597/LINE-Node-TypeScript-Gourmet/pull/1

ここからは実際にLINE Messaging APIを使って作成していきます。

簡単にここからの流れを説明します。

| クライアント            | LINE（バックエンド）                                                |
| ----------------------- | ------------------------------------------------------------------- |
| ①「お店を探す」をタップ |                                                                     |
|                         | ②「現在地を送る」ためのボタンメッセージを送信                       |
|                         | ③「お店を探す」以外のメッセージが届いたときにエラーメッセージを送信 |
| ④ 現在地を送る          |                                                                     |
|                         | ⑤ お店の配列を作成する                                              |
|                         | ⑥ 必要なデータのみにする                                            |
|                         | ⑦ 評価順に並び替えて上位 10 店舗にする                              |
|                         | ⑧ Flex Message を作成する                                            |
|                         | ⑨ お店の情報を Flex Message で送る                                  |
では作っていきましょう！
またこれら全てのコードを`api/controller/src/index.ts`に書くとコードが肥大化し可読性が落ちます。
なので**`Common`**ディレクトリに関数に切り分けて作成していきます。

またここからはLINEBotのオリジナルの型が頻出します。
1つずつ説明するのはあまりに時間がかかるので、知らない型が出てきたらその度に以下のサイトで検索するようにしてください。

https://github.com/line/line-bot-sdk-nodejs/blob/master/lib/types.ts

### ①「お店を探す」をタップ
こちらに関してはクライアント側の操作なので作業することはありません。

### ②「現在地を送る」ためのボタンメッセージを送信

#### ボタンメッセージのテンプレートを作成する
```typescript:api/controller/Common/TemplateMessage/YourLocation.ts
// Load the package
import { TemplateMessage } from '@line/bot-sdk';

export const yourLocationTemplate = (): TemplateMessage => {
  return {
    type: 'template',
    altText: '現在地を送ってください！',
    template: {
      type: 'buttons',
      text: '今日はどこでご飯を食べる？',
      actions: [
        {
          type: 'uri',
          label: '現在地を送る',
          uri: 'https://line.me/R/nv/location/',
        },
      ],
    },
  };
};
```

ちなみに以下のURLですが、LINEで利用できるURLスキームというもので位置情報を送れるものです。
https://line.me/R/nv/location/

詳しくは以下をご確認ください。

https://developers.line.biz/ja/docs/line-login/using-line-url-scheme/#sending-the-location-screen

#### ボタンメッセージの送信
```typescript:api/controller/src/Common/SendMessage/SendLocationOrError.ts
// Load the package
import { Client, WebhookEvent } from '@line/bot-sdk';

// Load the module
import { yourLocationTemplate } from '../TemplateMessage/YourLocation';

export const sendLocationOrError = async (client: Client, event: WebhookEvent): Promise<void> => {
  try {
    // If the message is different from the target, returned
    if (event.type !== 'message' || event.message.type !== 'text') {
      return;
    }

    // Retrieve the required items from the event
    const replyToken = event.replyToken;
    const text = event.message.text;

    // Perform a conditional branch
    if (text === 'お店を探す') {
      await client.replyMessage(replyToken, yourLocationTemplate());
    } else {
      // エラーメッセージを送る
    }
  } catch (err) {
    console.log(err);
  }
};
```

### ③「お店を探す」以外のメッセージが届いたときにエラーメッセージを送信
#### エラーメッセージのテンプレートを作成する
```typescript:api/controller/src/Common/TemplateMessage/ErrorMessage.ts
// Load the package
import { TextMessage } from '@line/bot-sdk';

export const errorMessageTemplate = (): TextMessage => {
  return {
    type: 'text',
    text: 'ごめんなさい、このメッセージは対応していません',
  };
};
```

#### エラーメッセージの送信
```typescript:api/controller/src/Common/SendMessage/SendLocationOrError.ts
// Load the package
import { Client, WebhookEvent } from '@line/bot-sdk';

// Load the module
import { yourLocationTemplate } from '../TemplateMessage/YourLocation';
import { errorMessageTemplate } from '../TemplateMessage/ErrorMessage';

export const sendLocationOrError = async (client: Client, event: WebhookEvent): Promise<void> => {
  try {
    // If the message is different from the target, returned
    if (event.type !== 'message' || event.message.type !== 'text') {
      return;
    }

    // Retrieve the required items from the event
    const replyToken = event.replyToken;
    const text = event.message.text;

    // Perform a conditional branch
    if (text === 'お店を探す') {
      await client.replyMessage(replyToken, yourLocationTemplate());
    } else {
      await client.replyMessage(replyToken, errorMessageTemplate());
    }
  } catch (err) {
    console.log(err);
  }
};
```

#### APIに登録する
これで②と③のメッセージが送信されます！

```typescript:api/controller/src/index.ts
// Load the package
import { Client, ClientConfig, MiddlewareConfig, middleware, WebhookEvent } from '@line/bot-sdk';
import express from 'express';
import dotenv from 'dotenv';
dotenv.config();

// Load the module
import { sendLocationOrError } from './Common/SendMessage/SendLocationOrError';

// Read the ports from process.env.file
const PORT = process.env.PORT || 3000;

// Load the access token and channel secret from the .env file
const clientConfig: ClientConfig = {
  channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '',
  channelSecret: process.env.CHANNEL_SECRET || '',
};
const middlewareConfig: MiddlewareConfig = {
  channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '',
  channelSecret: process.env.CHANNEL_SECRET || '',
};

// Instantiate
const app: express.Express = express();
const client: Client = new Client(clientConfig);

// Do routing
// Test
app.get('/', (req: express.Request, res: express.Response): void => {
  res.send('Hello World');
});

// API Routing
app.post(
  '/api/line/message',
  middleware(middlewareConfig),
  async (req: express.Request, res: express.Response): Promise<void> => {
    const events: WebhookEvent[] = req.body.events;

    events.map(
      async (event: WebhookEvent): Promise<void> => {
        try {
          await sendLocationOrError(client, event);

          // これでデバッグしつつ進めていくとよろし。
          // console.log(event);
        } catch (err) {
          console.log(err);
        }
      }
    );
  }
);

// Start the server
app.listen(PORT, (): void => {
  console.log('http://localhost:3000');
});
```

### ④ 現在地を送る 
こちらに関してはクライアント側の操作なので作業することはありません。

### ⑤ お店の配列を作成する
ここからGoogle MapのAPIを使っていきます！

https://developers.google.com/maps/?hl=ja

まずはGCPのコンソール画面に入って下さい。

https://cloud.google.com/

![スクリーンショット 2021-06-24 10.52.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/b4543d45-2c7e-2b5d-0b16-d15855f3a3c4.png)

コンソールに入ったらプロジェクトを作成しましょう！

![スクリーンショット 2021-06-24 10.53.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/2a32f30a-4dba-0adb-f8f7-f567a65d2f0a.png)

私は、`LINE-Node-TypeScript-Gourmet`で作成しました。

![スクリーンショット 2021-06-24 10.56.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/d34280ec-1028-a017-0860-5b1ff49fc46f.png)

では、ライブラリを有効化しましょう！
使うライブラリは2つです。
**Map JavaScript API**
**Places API**
お店検索をするAPIは「Places API」ですが、
JavaScriptから呼び出すために「Map JavaScript API」が必要となります。

![スクリーンショット 2021-06-24 11.02.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/c70fca90-9a33-5385-9cb5-7136ba1754d5.png)

![スクリーンショット 2021-06-24 11.03.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/87aec1a1-bb7a-1299-5efd-35179a60ac92.png)

ここまでできたら次にAPIを作成しましょう。

![スクリーンショット 2021-06-24 11.04.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/6c34129f-e578-f899-4805-259720391543.png)

APIは開発環境用と本番環境用の2つを作成しましょう。

これからの開発はこちらのAPIキーを使います。
![スクリーンショット 2021-06-24 11.06.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/2112c346-ff59-4641-6f3c-0d0622aafe26.png)

このAPIキーは本番環境で使います。
![スクリーンショット 2021-06-24 11.08.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/b473ec24-5149-3479-bcca-cc120fb4c449.png)

上記の説明でわからなければ以下のサイトを参考にされて下さい。

https://nendeb.com/276

この発行したAPIキーを`.env`に登録します。

```env:.env
# LINE（ご自分のBotの値に書き換えを行ってください）
CHANNEL_SECRET = "a9152"
CHANNEL_ACCESS_TOKEN = "zTon3"

# Google_map（ご自分の値に書き換えを行ってください）
GOOGLE_DEV_API = "AIzaS"
GOOGLE_PROD_API = "AIzaS"
```

また、今回は公式のパッケージを使用せずに作成します。

https://github.com/googlemaps/google-maps-services-js

型定義もできるみたいですが、Axiosで作った方がスムーズだと判断しました。

近隣のお店を調べるので、**Place Search**の**Nearby Search requests**を使います。

https://developers.google.com/maps/documentation/places/web-service/search?hl=ja#PlaceSearchRequests

```typescript:api/controller/src/Common/TemplateMessage/GoogleMap/GetGourmetInfo.ts
// Load the package
import axios, { AxiosResponse } from 'axios';

export const getGourmetInfo = async (latitude: number, longitude: number) => {
  try {
    // Google Map API
    const Google_API = process.env.GOOGLE_DEV_API;

    const url = `https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=${latitude},${longitude}&radius=800&type=restaurant&key=${Google_API}&language=ja`;
    const gourmets: AxiosResponse<any> = await axios.get(url);
    const gourmetData = gourmets.data.results;

    return gourmetData;
  } catch (err) {
    console.log(err);
  }
};
```

### ⑥ 必要なデータのみにする 
使うデータは以下の通りです。

| 必要なデータ          | 理由                                       |
| --------------------- | ------------------------------------------ |
| geometry_location_lat | 店舗案内のURLで使うため                  |
| geometry_location_lng | 店舗案内のURLで使うため                  |
| name                  | Flex Message内と店舗詳細のURLで使うため |
| photo_reference       | 店舗写真を生成するために使う               |
| rating                | Flex Message内で使うため                     |
| vicinity              | 店舗詳細のURLで使うため                   |

店舗詳細と店舗案内、店舗写真のURLはこの後解説します。

ということで必要なデータのみを抜き出して配列を再生成しましょう。

```typescript:api/controller/src/Common/TemplateMessage/GoogleMap/FormatGourmetData.ts
// Load the module
import { getGourmetInfo } from './GetGourmetInfo';

// types
import { RequiredGourmetData } from './type/FormatGourmetData.type';

export const formatGourmetData = async (latitude: number, longitude: number) => {
  try {
    const gourmetData = await getGourmetInfo(latitude, longitude);

    // Extract only the data you need
    const sufficientDataArray: any = gourmetData.filter(
      (gourmet: any) => gourmet.photos !== undefined || null
    );

    const requiredGourmetData: RequiredGourmetData = sufficientDataArray.map((gourmet: any) => {
      return {
        geometry_location_lat: gourmet.geometry.location.lat,
        geometry_location_lng: gourmet.geometry.location.lng,
        name: gourmet.name,
        photo_reference: gourmet.photos[0].photo_reference,
        rating: gourmet.rating,
        vicinity: gourmet.vicinity,
      };
    });

    return requiredGourmetData;
  } catch (err) {
    console.log(err);
  }
};
```

上記で、`RequiredGourmetData`という型を使用しているので型定義ファイルを作ります。

```typescript:api/controller/src/Common/TemplateMessage/GoogleMap/type/FormatGourmetData.type.ts
export type RequiredGourmetData = {
  geometry_location_lat: number;
  geometry_location_lng: number;
  name: string;
  photo_reference: string;
  rating: number;
  vicinity: string;
}[];

```

ちなみに私はこの⑥の内容が一番詰まりました。
理由としては、Google Mapで`photo_reference`が登録されていない店舗があったためです。
`gourmet.photos[0]`でエラーが出たので配列の取得間違ってるんかなと焦りました。。。
ということで今回は、店舗写真がないお店に関しては除外するようにしました。（`sufficientDataArray`）

### ⑦ 評価順に並び替えて上位10店舗にする
ここはそんなに難しくないです。
`sort`で並び替えて、`slice`で新たな配列を作ってあげましょう！

```typescript:api/controller/src/Common/TemplateMessage/GoogleMap/SortRatingGourmet.ts
// Load the module
import { formatGourmetData } from './FormatGourmetData';

// types
import { GourmetData, SortGourmetData } from './type/SortRatingGourmet.type';

export const sortRatingGourmet = async (
  latitude: number,
  longitude: number
): Promise<SortGourmetData | undefined> => {
  try {
    const gourmetData = await formatGourmetData(latitude, longitude);

    if (gourmetData === undefined) {
      return;
    }

    // Sort by rating
    gourmetData.sort((a: GourmetData, b: GourmetData) => b.rating - a.rating);

    // narrow it down to 10 stores.
    const sortGourmetData: SortGourmetData = gourmetData.slice(0, 10);

    return sortGourmetData;
  } catch (err) {
    console.log(err);
  }
};
```

型定義を行いましょう。

```typescript:api/controller/src/Common/TemplateMessage/GoogleMap/type/SortRatingGourmet.type.ts
export type GourmetData = {
  geometry_location_lat: number;
  geometry_location_lng: number;
  name: string;
  photo_reference: string;
  rating: number;
  vicinity: string;
};

export type SortGourmetData = GourmetData[];
```

### ⑧ Flex Messageを作成する
⑥で説明した必要なデータについて解説します。

| 必要なデータ          | 理由                                       |
| --------------------- | ------------------------------------------ |
| geometry_location_lat | 店舗案内のURLで使うため                  |
| geometry_location_lng | 店舗案内のURLで使うため                  |
| name                  | Flex Message内と店舗詳細のURLで使うため |
| photo_reference       | 店舗写真を生成するために使う               |
| rating                | Flex Message内で使うため                     |
| vicinity              | 店舗詳細のURLで使うため                   |

`name`と`rating`はFlex Message内で使います。

店舗詳細に関してですが、こちらのURLは以下となります。
https://maps.google.co.jp/maps?q=${店舗名}${住所}&z=15&iwloc=A

店舗案内に関しては以下のURLとなります。
https://www.google.com/maps/dir/?api=1&destination=${緯度},${経度}

店舗写真に関しては以下のURLとなります。
https://maps.googleapis.com/maps/api/place/photo?maxwidth=${任意の幅}&photoreference=${photo_reference}&key=${Google_API}

https://developers.google.com/maps/documentation/places/web-service/photos?hl=ja#place_photo_requests

ということで、Flex Message内でこれらのURLを生成していけば完成です。
やっていきましょう！

ちなみにここが2番目に詰まりました。
ここは型定義です。
Flex Messageは、`FlexMessage`という型なのですが、以下のサイトを見ればわかる通り、
`FlexContainer`, `FlexBubble`, `FlexCarousel`など様々なものに依存しているので、
これらをしっかり型定義しないと、FlexMessageだけ型定義してもエラーとなります。
JavaScriptではもっと省略して書けたのですが、TypeScriptの場合は順序を意識して書く必要がありました。
型エラーが出なければ安全に動くというのがわかるので、多少コードは長くなっても便利だなと思いました。

https://github.com/line/line-bot-sdk-nodejs/blob/master/lib/types.ts#L787

```typescript:api/controller/src/Common/TemplateMessage/GoogleMap/CreateFlexMessage.ts
// Load the package
import { FlexMessage, FlexCarousel, FlexBubble } from '@line/bot-sdk';

// Load the module
import { sortRatingGourmet } from './SortRatingGourmet';

// types
import { Gourmet } from './type/CreateFlexMessage.type';

export const createFlexMessage = async (
  latitude: number,
  longitude: number
): Promise<FlexMessage | undefined> => {
  try {
    const sortGourmetData = await sortRatingGourmet(latitude, longitude);

    if (sortGourmetData === undefined) {
      return;
    }

    // FlexMessage
    const FlexMessageContents: FlexBubble[] = await sortGourmetData.map((gourmet: Gourmet) => {
      // Google Map API
      const Google_API = process.env.GOOGLE_DEV_API || undefined;

      // Create a URL for a store photo
      const photoURL = `https://maps.googleapis.com/maps/api/place/photo?maxwidth=400&photoreference=${gourmet.photo_reference}&key=${Google_API}`;

      // Create a URL for the store details
      const encodeURI = encodeURIComponent(`${gourmet.name} ${gourmet.vicinity}`);
      const storeDetailsURL = `https://maps.google.co.jp/maps?q=${encodeURI}&z=15&iwloc=A`;

      // Create a URL for store routing information
      const storeRoutingURL = `https://www.google.com/maps/dir/?api=1&destination=${gourmet.geometry_location_lat},${gourmet.geometry_location_lng}`;

      const flexBubble: FlexBubble = {
        type: 'bubble',
        hero: {
          type: 'image',
          url: photoURL,
          size: 'full',
          aspectMode: 'cover',
          aspectRatio: '20:13',
        },
        body: {
          type: 'box',
          layout: 'vertical',
          contents: [
            {
              type: 'text',
              text: gourmet.name,
              size: 'xl',
              weight: 'bold',
            },
            {
              type: 'box',
              layout: 'baseline',
              contents: [
                {
                  type: 'icon',
                  url:
                    'https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png',
                  size: 'sm',
                },
                {
                  type: 'text',
                  text: `${gourmet.rating}`,
                  size: 'sm',
                  margin: 'md',
                  color: '#999999',
                },
              ],
              margin: 'md',
            },
          ],
        },
        footer: {
          type: 'box',
          layout: 'vertical',
          contents: [
            {
              type: 'button',
              action: {
                type: 'uri',
                label: '店舗詳細',
                uri: storeDetailsURL,
              },
            },
            {
              type: 'button',
              action: {
                type: 'uri',
                label: '店舗案内',
                uri: storeRoutingURL,
              },
            },
          ],
          spacing: 'sm',
        },
      };

      return flexBubble;
    });

    const flexContainer: FlexCarousel = {
      type: 'carousel',
      contents: FlexMessageContents,
    };

    const flexMessage: FlexMessage = {
      type: 'flex',
      altText: 'this is a flex message',
      contents: flexContainer,
    };

    return flexMessage;
  } catch (err) {
    console.log(err);
  }
};
```

型定義を行いましょう。

```typescript:api/controller/src/Common/TemplateMessage/GoogleMap/type/CreateFlexMessage.type.ts
export type Gourmet = {
  geometry_location_lat: number;
  geometry_location_lng: number;
  name: string;
  photo_reference: string;
  rating: number;
  vicinity: string;
};
```

### ⑨ お店の情報を Flex Messageで送る
#### Flex Messageの送信
```typescript:api/controller/src/Common/SendMessage/SendGourmetMessage.ts
// Load the package
import { Client, WebhookEvent } from '@line/bot-sdk';

// Load the module
import { createFlexMessage } from '../TemplateMessage/GoogleMap/CreateFlexMessage';

export const sendGourmetMessage = async (client: Client, event: WebhookEvent) => {
  try {
    // If the message is different from the target, returned
    if (event.type !== 'message' || event.message.type !== 'location') {
      return;
    }

    // Retrieve the required items from the event
    const replyToken = event.replyToken;
    const latitude = event.message.latitude;
    const longitude = event.message.longitude;

    const flexMessage = await createFlexMessage(latitude, longitude);

    if (flexMessage === undefined) {
      return;
    }

    await client.replyMessage(replyToken, flexMessage);
  } catch (err) {
    console.log(err);
  }
};
```

#### APIに登録する
```typescript:api/controller/src/index.ts
// Load the package
import { Client, ClientConfig, MiddlewareConfig, middleware, WebhookEvent } from '@line/bot-sdk';
import express from 'express';
import dotenv from 'dotenv';
dotenv.config();

// Load the module
import { sendLocationOrError } from './Common/SendMessage/SendLocationOrError';
import { sendGourmetMessage } from './Common/SendMessage/SendGourmetMessage';

// Read the ports from process.env.file
const PORT = process.env.PORT || 3000;

// Load the access token and channel secret from the .env file
const clientConfig: ClientConfig = {
  channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '',
  channelSecret: process.env.CHANNEL_SECRET || '',
};
const middlewareConfig: MiddlewareConfig = {
  channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '',
  channelSecret: process.env.CHANNEL_SECRET || '',
};

// Instantiate
const app: express.Express = express();
const client: Client = new Client(clientConfig);

// Do routing
// Test
app.get('/', (req: express.Request, res: express.Response): void => {
  res.send('Hello World');
});

// API Routing
app.post(
  '/api/line/message',
  middleware(middlewareConfig),
  async (req: express.Request, res: express.Response): Promise<void> => {
    const events: WebhookEvent[] = req.body.events;

    events.map(
      async (event: WebhookEvent): Promise<void> => {
        try {
          await sendLocationOrError(client, event);
          await sendGourmetMessage(client, event);
        } catch (err) {
          console.log(err);
        }
      }
    );
  }
);

// Start the server
app.listen(PORT, (): void => {
  console.log('http://localhost:3000');
});
```

これで完成です。

ここまでの作業は以下のブランチで行っています。

https://github.com/ssk9597/LINE-Node-TypeScript-Gourmet/pull/2

ブランチを見ていただければわかると思うのですが、
最初は「車か徒歩か」などで分岐させお店の検索範囲を変えようと思ったのですが、
この要件ではDBを必要としたので一旦DBなしの構成で作成してみました。

この後、ジャンル検索やお気に入り店舗の登録などの機能も作っていく予定です。

## では最後にデプロイしましょう！
今回もデプロイは`Glitch`を使います。

https://glitch.com/

アカウントは、Githubで作るのがおすすめです。
作成しましたら、プロジェクトを作成します。
**「import from GitHub」**をクリックします。

![スクリーンショット 2021-06-14 21.26.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/392a4d01-ab7e-3e09-2c75-0b348ea2f87c.png)

ここには、GithubのURLを貼り付けます。
![スクリーンショット 2021-06-24 12.06.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/d0204dda-f477-603b-f124-54a02a64ef7f.png)

少し待つとこのように読み込まれます。
![スクリーンショット 2021-06-24 12.11.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/666680bd-762d-1323-7937-e4ae306ca3ef.png)

最後に`.env`に値を入力します。
![スクリーンショット 2021-06-24 12.09.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/6690e2a8-3992-86a9-d0cf-39364a2e0e0c.png)

ここまで行えばデプロイは成功です！
![スクリーンショット 2021-06-24 12.11.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/d258c0f3-dc04-b965-8e15-3ac50e6aedbe.png)

## Webhookの設定を変更

![スクリーンショット 2021-06-24 12.12.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/4ce9d54c-26b2-eff6-95c8-d2bb42eddde4.png)

## 最後に
次はDBを使い、このアプリをより良いものにしていきたいです。
AWS Lambdaなどを使うか、Firebaseを使うか迷いますな。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/ca6c86f2-8bee-b9c2-dc9d-5ea491caddc5.png)

ここまで読んでいただきありがとうございました！