Cloudflare スタックを駆使してQBハウスの待ち人数推移を眺める

# 目的
QBハウスがもたらす「省時間」効果を最大限に享受するには、QBハウスでの「待ち時間」を最小限にする必要があります。

> [QB HOUSEとは? | QB Lab | QBハウス "10分の身だしなみ"](https://www.qbhouse.co.jp/qblab/about/)
QB HOUSE が考える「省時間」とは、同じ社会的・経済的効果や便益を、
より少ない時間で得られるヘアカットサービスを提供することです。
このクオリティを高めていくために、私たちは多様な取り組みをスタートしています。

そのため、公式サイトでは店舗ごとの待ち人数を確認することができます。

>[ご利用案内 | QBハウス "10分の身だしなみ"](https://www.qbhouse.co.jp/guidance/)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/b25942bf-c0f5-7bad-ad89-fd9a74de63c0.png)

>[渋谷南口店 | 店舗検索結果 | QBハウス "10分の身だしなみ"](https://www.qbhouse.co.jp/search/488)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/017838ba-e185-dd35-7561-d1b0cfbccfdc.png)

ただ、もったいないのは「現在の」待ち人数しか確認できません。

[QB PREMIUM](https://www.qbpremium.com/) では、アプリ予約ができるようですが、始まったばかりのサービスのため、QBハウスで待ち時間なく対応してもらうためには、ユーザ自身で工夫が必要です。

今回は、このサイトのデータを使って待ち人数の推移を可視化することで、空いている時間帯の目測を立てられることを目的とします。

# 成果物

https://busy-qbhouse.pages.dev/

https://github.com/kyouheicf/busy-qbhouse/tree/main

開店直後はとても混んでいるようなので、11:00-12:00 に早めの昼食を済ませるついでにQBハウスに寄るなどの有効活用が考えられます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/5006a16e-aafb-b35a-6a3d-43aa16c7175b.png)

# Cloudflare スタック

練習も兼ねて色々なコンポーネントを使ってみます。

ブラウザと Cloudflare だけあれば、色々とやりたいことができるのがいいところです。

また、使いたい機能も Workers に Binding をするだけですぐに使えるようになります。

```mermaid
flowchart LR
b[Browser] --> |GET html|p
subgraph Cloudflare
p[Pages]
cw --> |Cron Trigger|cw[Cron Workers]
fw --> |"READ"|d["D1</br>(Table 'WaitPeople')"]
end
cw --> |"GET html</br>(Browser Rendering)"|q[www.qbhouse.co.jp]
cw --> |"WRITE"|d
b --> |"ajax Request</br>(drawChart)"|fw[Fetch Workers]
```

# Cron Workers

以下の Cron Trigger の記述により、5分ごとに起動させます。

```toml:wrangler.toml
[triggers]
crons = [ "*/5 * * * * " ]
```

## Browser Rendering

- [Browser Rendering · Browser Rendering docs](https://developers.cloudflare.com/browser-rendering/)
- [Cloudflare Workers Browser Rendering API を使ってスクリーンショットを R2 に保存する - Qiita](https://qiita.com/khayama/items/51074d2476ad49d7e27c#workers-%E3%82%B3%E3%83%BC%E3%83%89)
- [PuppeteerによるヘッドレスChromeの使い方 evaluate | iwb.jp](https://iwb.jp/puppeteer-headless-chrome-evaluate/)

上記リンクを参考にして、待ち人数を定期的にヘッドレスブラウザから取得するコードです。

（今回の用途では、Browser Rendering である必要はないかもしれません）

```javascript:
// binding in wrangler.toml: browser = { binding = "MYBROWSER" }
const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();
// xxx は店舗ページのID
await page.goto("https://www.qbhouse.co.jp/search/xxx", {
    //一定時間ネットワーク通信のないことで完了を判定する
    waitUntil: "load",
});
// セレクタで待ち人数の要素を HTML から取得
const html = await page.$eval('#salon_info > div > div:nth-child(4) > div.waiting_wrap > dl.number > dd', node => node.innerText);
//console.log(`shop status in html === ${html}`)
...
// D1 WRITE 処理
...
await browser.close();
```

## D1
オープンベータになりましたね。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">D1 オープンベータ(2024Q1GA予定)！Workers 有料プランでは、DBあたり2GBかつ25 個までOK。過去30日間1分単位のタイムトラベルがデフォルトで有効化済み。Byte ベースから行ベースの課金モデルへの改良。Workers for Platform では顧客ごとに実質無限にDBを作って分けられる！<a href="https://t.co/Egaty0vuNt">https://t.co/Egaty0vuNt</a></p>&mdash; kyhayama (@kyhayama) <a href="https://twitter.com/kyhayama/status/1707404221577117966?ref_src=twsrc%5Etfw">September 28, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

### データベース準備
以下のコマンドでテーブルを用意します。

```shell:
#データベース作成
wrangler d1 create QBHOUSE
#テーブル作成
wrangler d1 execute QBHOUSE --command "CREATE TABLE IF NOT EXISTS WaitPeople (DateTime DATE PRIMARY KEY, WaitPeopleNumber INTEGER)"
#テーブル一覧取得
wrangler d1 execute QBHOUSE --command "SELECT name FROM sqlite_schema WHERE type ='table'"
#レコード一覧取得
wrangler d1 execute QBHOUSE --command="SELECT * FROM WaitPeople"
#┌─────────────────────┬──────────────────┐
#│ DateTime            │ WaitPeopleNumber │
#├─────────────────────┼──────────────────┤
#│ 2023-10-02 14:35:00 │ 4                │
#├─────────────────────┼──────────────────┤
#...
#日付ごとのレコード一覧取得
wrangler d1 execute QBHOUSE --command="SELECT substr(DateTime,12,2) AS hour, substr(DateTime,15,2) AS min, substr(DateTime,18,3) AS sec, WaitPeopleNumber AS num FROM WaitPeople WHERE DATE(DateTime) = '2023-10-02'"
#┌──────┬─────┬─────┬──────────────────┐
#│ hour │ min │ sec │ num │
#├──────┼─────┼─────┼──────────────────┤
#│ 14   │ 35  │ 00  │ 4                │
#├──────┼─────┼─────┼──────────────────┤
#...
```

### データ書き込み

- [D1 client API · Cloudflare D1 docs](https://developers.cloudflare.com/d1/platform/client-api/)

以下のようなコードでシンプルに書き込みが可能です。

（データが溜まってきたら、定期的に古いデータを削除する仕組みが必要かもしれません）

- [JavaScript Date as in YYYY-MM-DD hh:mm:ss Format or MM/DD/YYYY hh:mm:ss | by Yogesh D V | Medium](https://trymysolution.medium.com/javascript-date-as-in-yyyy-mm-dd-hh-mm-ss-format-or-mm-dd-yyyy-hh-mm-ss-a0c96e8fa888)
- [JavaScript で実行環境に左右されず常に JST 日本時間を取得する - Neo's World](https://neos21.net/blog/2020/12/09-01.html)

```javascript:
if (html != '開店前' && html != '受付終了') {
		// html is 4\n人
		await env.DB
			.prepare('INSERT INTO WaitPeople (DateTime, WaitPeopleNumber) VALUES (?1, ?2)')
			.bind(dateInYyyyMmDdHhMmSs(jstNow), html.slice(0, 1))
			.run()
	}
```

# Fetch Workers
`?date=2023-10-03` のようにクエリをつけると、日付ごとの JSON データを返してくれる API を作成します。

## JSON Response
以下のコードで D1 から読み込んだデータを JSON に変換してレスポンスします。

```javascript:
// Get query parameter like ?date=2023-10-03
const { searchParams } = new URL(request.url)
let date = searchParams.get('date')
// Get data from D1 database
const stmtstring = `SELECT substr(DateTime,12,2) AS hour, substr(DateTime,15,2) AS min, substr(DateTime,18,3) AS sec, WaitPeopleNumber as num FROM WaitPeople WHERE DATE(DateTime) = '${date}'`
const stmt = env.DB.prepare(stmtstring);
// Transform Data into JSON
const { results } = await stmt.all();
const json = JSON.stringify(results, null, 2);

// Return JSON Response 
return new Response(json, {
    headers: {
        "content-type": "application/json;charset=UTF-8",
        'Access-Control-Allow-Origin': 'https://busy-qbhouse.pages.dev'
    },
});
```

## CORS
以下のコードを流用しました。

- [CORS header proxy · Cloudflare Workers docs](https://developers.cloudflare.com/workers/examples/cors-header-proxy/)

# Pages
- [Use Direct Upload with continuous integration · Cloudflare Pages docs](https://developers.cloudflare.com/pages/how-to/use-direct-upload-with-continuous-integration/#get-credentials-from-cloudflare)

以下のフォルダと HTML ファイルを作成し、アップロードします。

```shell:
busy-qbhouse-pages/
└── index.html
```

```shell:
wrangler pages project create busy-qbhouse
wrangler pages publish .
```

## `drawChart` (Google Chart API)
- [ChatGPTを使ってブログに載せるグラフのスクリプトを作成する | 橋本総研.com](https://hasimoto-soken.com/archives/6221)

これは感動しました。HTML のような、以前からコードの書き方が広く知れ渡っていて、最新情報に依存しにくい部分はChatGPTを使って基本形を作ってもらって、そこから最新の記述方式に変えていくことで効率的に実装できました。

```text:ChatGPTに投げたプロンプト1
AJAXを使用してデータを取得し、Google Chart API で折れ線グラフを書くためのhtmlコードを書いてください。
・タイトルは「QBハウスxxx店の待ち人数推移」
・縦軸の名称は「待ち人数」
・縦軸の範囲は0から10
・横軸の名称は「時間」
・横軸のタイプは timeofday
・横軸の範囲は09:00から20:00
・幅は700px

・軸ラベルを表示して。

・各要素の線の色は洒落た感じで。

・コードはなるべく簡潔に。
```

また、時系列データに関するオプション等は以下のサイトを参考にしました。

- [Chart.js で時系列データを表示したときのメモ](https://zenn.dev/hasegawasatoshi/scraps/da0f2fea5a6c31)
- [【Chart.js】時系列(タイムライン)のグラフを表示させる方法 | YukiPress](https://mat0401.info/blog/chartjs-timeline/)

# まとめ
どこかから定期的に情報と取ってきてデータを貯める、その後ほしいデータを可視化するパターンは、このような形で手軽に実装できることがわかりました。

実際にやってみると、Cloudflare だけあれば何の不自由もなくできてしまいます。
やはり圧倒的な開発者体験でした。