いつかリアルでも九州温泉道八十八湯巡りをしてみたい！擬似巡り好きな私に毎日、温泉情報を教えてくれるボットを試作してみた！

## 作ろうと思った理由
毎日、お昼休みにウォーキングをしているのですが、続ける上で何か具体的な目標があった方が楽しいかも、モチベーションが上がるかもって思って、2013年くらいからウォーキングで歩いた距離をGPSアプリ(Endomondo、MapMyRun)で記録し、記録した毎日の距離と四国お遍路八十八か所巡りの距離をエクセルで紐づけて、本当は行ってないけど、行った気になるウォーキングを楽しんでます。

![test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217319/004d3f25-7601-d938-3136-62875b1f6fc8.png)

1年半くらいで目標達成し、2016年から、同じ八十八つながりということで九州温泉道八十八湯巡りに挑戦し、現在も続けております。

この活動で初めて知ったのですが、訪れた施設数が増えるとスタンプがたまるというシステムがあるらしいです。たくさんあるけば、たくさんの施設を訪れることができ、擬似巡り上では、現在、四段まできました。もうすぐ、風゜呂(ぷろ)で全部あつまると泉人(せんにん)の称号が得られるらしいです！泉人の称号を目指し、毎日楽しみながらウォーキングに励んでます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217319/b0201ffe-56d3-e563-492d-458dd2e17d01.png)

ただ一つ、心のどこかで気になっていたことがあります。せっかくならもっと温泉施設のことも知った方がより楽しくなるかもと思いました。でも、毎日、Googleで手入力して検索するのはちょっと続かない。そんなわけで、毎日のウォーキングをさらに楽しめるよう、私が擬似的に訪れている、あるいは今後訪れうる温泉地の情報をGoogle Mapの情報でLINE 通知してくれるボットを作ってみようと思いました！


## 作品紹介
毎日お昼休みに九州温泉道八十八湯の宿泊施設情報をLine通知してくれるボットを作ってみました！
![作品.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217319/af165543-7994-0d9d-b030-200e2ab797e9.png)


## 仕組み
・温泉地の情報を取得するAPI：Places API
・コード：node.js (v18.16.0)
・LINE通知：LINE Notify

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217319/4530ced9-b217-1bbc-e8fa-69dfcf710651.png)


## ChatGPTの活用
APIの取得の仕方の部分、苦手意識があったので今回、自分が思い描いた作品を実装するにあたり今回、ChatGPTを活用しています。コードも沢山聞きながら試しました。なかなかすごい返答を返してくるなと思いました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217319/e728cd16-ac5a-d9d9-4927-56817e37ff04.png)

質問して実装、動かなかったら修正。修正の仕方がわからなければそれもChatGPTに聞いてみる、このサイクルを繰り返しなんとか形になりました。

## コード (温泉地情報の取得 notifyOnsenRyokan.js）


<!--START_CODE_BLOCK-->

``` javascript
/**
 * ************************************************************
 * 取得したい旅館名が入ったテキストデータを読み込んでリストに格納する
 * ************************************************************
 */

const fs = require('fs');
const axios = require('axios');
const filePath = 'ryokanNames.txt';

function readTextFileToList() {
  try {
    const fileData = fs.readFileSync(filePath, 'utf-8');
    const lines = fileData.split('\n').map(line => line.trim()).filter(line => line !== '');
    return lines;
  } catch (error) {
    console.error('テキストファイルの読み込みエラー:', error);
    return [];
  }
}

const ryokanNames = readTextFileToList();
const randomIndex = Math.floor(Math.random() * ryokanNames.length);



/**
 * ************************************************************
 * Google Places APIを使用して施設を検索し、MAP URLを生成
 * ************************************************************
 */

// Google Places APIのキー
const apiKey = 'YOUR_API_KEY';
// Line Notifyのアクセストークン
const lineNotifyToken = 'YOUR_ACCESS_TOKEN';

// Google Places APIを使用して施設を検索し、詳細情報を取得する関数
async function searchPlaceDetails(placeName) {
  try {
    const response = await axios.get('https://maps.googleapis.com/maps/api/place/findplacefromtext/json', {
      params: {
        key: apiKey,
        input: placeName,
        inputtype: 'textquery',
        fields: 'name,formatted_address,geometry,place_id',
      },
    });

    if (response.data && response.data.candidates && response.data.candidates.length > 0) {
      const place = response.data.candidates[0];
      return place;
    }

    throw new Error('Place not found.');
  } catch (error) {
    throw new Error('Failed to retrieve place details.');
  }
}

// マップのURLを生成する関数
function generateMapUrl(place) {
  const { formatted_address, place_id, geometry } = place;
  const mapUrl = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
    formatted_address
  )}&query_place_id=${place_id}&center=${geometry.location.lat},${geometry.location.lng}`;
  return mapUrl;
}

/**
 * ************************************************************
 * Line notifyで通知
 * ************************************************************
 */

// Line Notifyに通知する関数
async function notifyLine(message) {
  try {
    await axios.post('https://notify-api.line.me/api/notify', `message=${encodeURIComponent(message)}`, {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        Authorization: `Bearer ${lineNotifyToken}`,
      },
    });
  } catch (error) {
    throw new Error('Failed to send Line Notify.');
  }
}

// メインの処理
async function main() {
  const placeName = ryokanNames[randomIndex];

  try {
    const placeDetails = await searchPlaceDetails(placeName);
    const mapUrl = generateMapUrl(placeDetails);

    await notifyLine(`毎日お疲れ様です！本日のお宿はこちら「${placeName}」: ${mapUrl}`);
    console.log('Line Notify sent successfully.');
  } catch (error) {
    console.error('Error:', error.message);
  }
}

// メイン処理を実行
main();

```
<!--END_CODE_BLOCK-->


## コード (定期実行 executeEveryday.js)

``` javascript
/**
 * ******************************************
 * 毎日12時20分にLine notifyで定期実行する
 * ******************************************
 */

const cron = require('node-cron');
const { exec } = require('child_process');

// cronジョブを定義して実行する関数
function scheduleCronJob() {
  // cronジョブを毎日12時30分に設定
  cron.schedule('30 12 * * *', () => {
    console.log('Executing notifyOnsenRyokan.js');

    // notify.jsファイルを実行
    exec('node notifyOnsenRyokan.js', (error, stdout, stderr) => {
      if (error) {
        console.error('Execution error:', error.message);
      }
      if (stdout) {
        console.log('Standard output:', stdout);
      }
      if (stderr) {
        console.error('Standard error:', stderr);
      }
    });
  }, {
    scheduled: true,
    timezone: 'Asia/Tokyo' // タイムゾーンを指定
  });
}

// メインの処理を実行
scheduleCronJob();
```

## 動作の様子
リアルタイムの様子ではないですが、別途動画撮影しましたので共有致します。
毎日、温泉アドバイザーからお昼にLINEが来てます。
(実際にお昼休みに撮影してたのですが、カメラを取ってる自分の様子が携帯に映りこんでしまってたので、別途撮影しました。）

<iframe width="1280" height="838" src="https://www.youtube.com/embed/_js4ZzRvz5E" title="マイビデオ1" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>


## まとめ

今年の4月半ばからJavascriptを学び始めましたので、結構、分からないところだらけで最初動かなくてめちゃめちゃ苦労しましたが、ChatGPTに聞いたり、実際に手を動かしてみたりしながらなんとか形になりました。私が知りたかった温泉地の情報のGoogleMapのリンクが届いたときには、本当に感動しました。大変なこともあるかもですが、この気持ちを忘れずこれからも色々作ってみたいなと思いました。


## 余談
今回の記事では、最初APIを叩くときにURLを叩く方法をスムーズに思いついたかのように書いてますが、実は最初の原型は、写真と施設名、住所の文字情報と画像だけを出す形のものでした。実際にやってみて、場所が文字情報だけなのは、何か残念、MapsAPIの良さが全然活かせてないって思って、公式ページを見て、URLで使うのが良さそうって思って、大幅修正しました。最初から、完璧でなくてもまずは作って、使って、直すのサイクルを回すのが大事なのかなと感じました。有難うございます。