🍶

【個人開発】面倒な二次会の店選びをサポートするサービスをリリースしました

に公開

はじめに

はじめまして、たつやと申します。

二次会の店選びって面倒じゃないでしょうか?面子にもよると思いますが個人的には「高くなければどこでもいいなー」と思ってしまいます。そんな人が利用するサービスTsugicocoを開発・リリースしました。

直近に飲み会があって二次会の店選びが面倒なときは使ってみていただけると嬉しいです!

https://tsugicoco.com/

画面

サービス概要

私のような二次会の店選びが面倒な人用のサービスです。現在地から500mあるいは1km圏内の営業時間内の居酒屋からランダムで1件ピックアップします。ユーザは電話番号から電話をして、空いていれば案内開始ボタンを押して店に向かいます。

たったこれだけのサービスです。

技術スタック

フロントエンド

React

Tailwind CSS

TypeScript

バックエンド

Next.js

サーバー

CloudFlare Pages

Next.jsを使うとCloudFlare Pagesにデプロイする際にedge runtime関連でエラーを吐くことは確認していましたが、このサービスはSSRを使っていないため発生しませんでした。

もし発生した場合はNext.jsのバージョンを下げることで解決できます。
https://zenn.dev/tacchan5424/articles/7a85aa6172e566

Nearby Search

現在地周辺の店情報を取得するためにGoogleNearby 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アカウント
https://x.com/Mw1FIDsnqdLbyDs

▼ Webサービス
https://bloomsurvey.com/

https://wikimiru.com/

▼ 本
https://zenn.dev/tacchan5424/books/22d87ed6bc8550

Discussion