はじめに
はじめまして、たつやと申します。
二次会の店選びって面倒じゃないでしょうか?面子にもよると思いますが個人的には「高くなければどこでもいいなー」と思ってしまいます。そんな人が利用するサービスTsugicocoを開発・リリースしました。
直近に飲み会があって二次会の店選びが面倒なときは使ってみていただけると嬉しいです!
画面
黒く伏せているところには店名と電話番号、住所です。
サービス概要
私のような二次会の店選びが面倒な人用のサービスです。現在地から500mあるいは1km圏内の営業時間内の居酒屋からランダムで1件ピックアップします。ユーザは電話番号から電話をして、空いていれば案内開始ボタンを押して店に向かいます。
たったこれだけのサービスです。
WebサービスですがGPS
を使う都合上、エージェントを偽装しない限りモバイルからしか動作しないようになっています。
技術スタック
フロントエンド
★ React
バックエンド
★ Next.js
サーバー
Next.js
を使うとCloudFlare Pages
にデプロイする際にedge runtime
関連でエラーを吐くことは確認していましたが、このサービスはSSR
を使っていないため発生しませんでした。
もし発生した場合はNext.js
のバージョンを下げることで解決できます。
Nearby Search
現在地周辺の店情報を取得するためにGoogle
のNearby Searchというサービスを利用しています。
このサービスでは以下例のように周辺情報を取得します。
※ほぼ実際に使っているコードで、一部加工しています。
const data = await fetch(
"https://places.googleapis.com/v1/places:searchNearby",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Goog-Api-Key": `${process.env.API_KEY}`,
"X-Goog-FieldMask":
"places.displayName,places.formattedAddress,places.priceRange,places.nationalPhoneNumber,places.priceRange,places.regularOpeningHours",
},
body: JSON.stringify({
includedTypes: ["bar", "japanese_restaurant"],
excludedTypes: [
"ramen_restaurant"
],
languageCode: "ja",
rankPreference: "DISTANCE",
locationRestriction: {
circle: {
center: {
latitude: Number(latitude),
longitude: Number(longitude),
},
radius: Number(distance),
},
},
}),
},
);
ヘッダー
-
X-Goog-Api-Key
:
APIキーを設定するためのものです。 -
X-Goog-FieldMask
:
レスポンスとして必要なフィールド名を設定するためのものです。例えばplaces.nationalPhoneNumber
を設定することで電話番号が取得できます。取得するフィールドに応じて課金のされ方が異なり、最も高い課金体系のフィールドに依存した課金が行われます。例ではplaces.nationalPhoneNumber
ベースで課金されます。詳しくはこちらとこちら。
ボディ
-
includedTypes
:
設定されているタグを持つ周辺の情報を取得します。bar
だけでは居酒屋がほとんどヒットしせず、japanese_restaurant
もセットするとかなりヒットするようになりました。or
条件で取得します。 -
excludedTypes
:
取得しないタグを設定します。japanese_restaurant
というタグを設定したことによって、居酒屋以外のあらゆる店がヒットするようになりました。そのため、居酒屋以外の飲食店が持っているタグを設定して除外しています。例ではramen_restaurant
のみですが、カレー屋や焼き肉屋、すし屋などもヒットしてしまうので実際には他にもいくつか設定しています。 -
rankPreference
:
取得順です。距離順で取得するようにしていますが、ランダムで1件抽出しているためこのサービスとしては不要なパラメータです。 -
locationRestriction
:
現在地と周辺を表すパラメータです。この例ではGPS
から取得した緯度経度を使って、かつ画面から設定した距離(500m or 1km)圏内を指定しています。
困りごと
記載した通り居酒屋固有のタグがないため、居酒屋だけを簡単に抽出することができません。居酒屋と居酒屋以外の飲食店で同じタグが設定されているケースが多くあるため、タグだけで制御しきれませんでした。例えば某餃子とカレーの店もヒットしてしまいます。
そのためBANリスト的なものを作って、ヒットしてしまった居酒屋以外の飲食店を追加でフィルタリングしています。私が確認した飲食店やお問い合わせのあった飲食店をBANリストに入れていって、少しずつ完璧に近づけていけたらと思っています。
他にも、ヒットするものの電話番号がないデータや金額の情報がないデータなどこのサービス上で表示するべきではない居酒屋もヒットしています。これらについても追加でフィルタリングしています。
モバイル限定
このようにエージェントでモバイルかどうかを判断しています。サービスのレイアウトもモバイル用にしか調整していません。二次会の居酒屋選びに特化して利用してもらいたいサービスであるためです。
const userAgent = req.headers.get("user-agent") || "";
const isMobile = /iPhone|Android|Mobile/.test(userAgent);
案内
案内開始ボタンを押すことでGoogle Map
が開いて案内までできます。これを実現するにあたって以下のようにURLを組み立てています。
const mapsUrl = `https://www.google.com/maps/dir/?api=1&origin=${緯度},${経度}&destination=${店名}&travelmode=walking`
実際店名だけでは特定できないケースがあるので、encodeURIComponent(``${name} ${address}``)
のようにして店名+住所をエンコードしています。
ボタン押下でwindow.open(mapsUrl, "_blank");
を実行するようにすれば完成です。以下のようなイメージです。
<button
className="w-1/3 h-10 rounded-lg bg-[#96B6C5] active:bg-[#ADC4CE] text-white font-extrabold"
onClick={() => {
const mapsUrl = `https://www.google.com/maps/dir/?api=1&origin=${緯度},${経度}&destination=${encodeURIComponent(`${name} ${address}`)}&travelmode=walking`;
window.open(mapsUrl, "_blank");
}}
>
案内開始
</button>
おわりに
小さなサービスなのであまりためになることをまとめられませんでしたが、一部でも誰かのためになれば幸いです。
もしよければXのフォローや他サービスも触っていただけると嬉しいです!
▼ Xアカウント
▼ Webサービス
▼ 本