1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【個人開発】ランチ迷子を救う!Google Places API (New) でランチルーレットWebアプリを作った

1
Last updated at Posted at 2026-04-06

はじめに

「今日のランチどうしよう…」と毎日悩んでいませんか?

そんな悩みを解決するために、現在地周辺のレストランをランダムに3軒ピックアップしてルーレット風に表示するWebアプリを作りました。

使ってみてよければ、いいねやコメントたくさん下さい!!

デモ: https://arupaka610.github.io/lunch-roulette/

スクリーンショット 2026-04-06 225756.png


作ったもの

機能一覧

機能 内容
📍 現在地取得 ブラウザのGeolocation APIでGPS取得
🔍 範囲指定検索 300m / 500m / 1km / 2km から選択
🍜 ジャンルフィルター 和食・ラーメン・寿司・中華など12種
💴 予算フィルター ¥〜¥¥¥¥ の4段階
🎲 ルーレット演出 3枚のカードが順番にアニメーション表示
🗺️ Google マップ連携 選んだ店をそのままマップで開ける
💬 口コミ表示 最新3件の口コミを表示

画面構成

① 検索画面  →  ② ルーレット画面  →  ③ 詳細画面
(条件設定)    (3店舗が順番に出現)    (店名・評価・口コミ・マップ)

技術スタック

  • フロントエンド: HTML / CSS / Vanilla JavaScript(フレームワークなし)
  • API: Google Maps JavaScript API + Places API (New)
  • ホスティング: GitHub Pages
  • CI/CD: GitHub Actions
  • 広告: Google AdSense

フレームワークを使わずに作ったので、ファイル3つ(index.html / style.css / app.js)のシンプルな構成です。


ポイント①:Google Places API (New) を使う

旧APIと新APIの違い

今回は旧Places APIではなく Places API (New) を使いました。

比較 旧API 新API (New)
エンドポイント maps.googleapis.com/maps/api/place/... places.googleapis.com/v1/...
JSでの呼び出し PlacesService.nearbySearch() Place.searchNearby()
非同期 コールバック async/await

新APIは async/await で書けてスッキリします。

検索の実装

// ジャンルなし → searchNearby
const { places } = await google.maps.places.Place.searchNearby({
  fields: ['id', 'displayName', 'rating', 'priceLevel', 'regularOpeningHours'],
  locationRestriction: {
    center: new google.maps.LatLng(lat, lng), // LatLngオブジェクトが必要
    radius: 500,
  },
  includedPrimaryTypes: ['restaurant'],
  maxResultCount: 20,
});

// ジャンルあり → searchByText(locationBias を使う)
const { places } = await google.maps.places.Place.searchByText({
  textQuery: `${genre} 飲食店`,
  fields: ['id', 'displayName', 'rating', 'priceLevel', 'regularOpeningHours'],
  locationBias: {
    center: new google.maps.LatLng(lat, lng),
    radius: 500,
  },
  maxResultCount: 20,
});

ハマりポイント①:locationRestriction vs locationBias

searchNearbylocationRestriction で円形指定できますが、searchByTextlocationRestriction矩形(LatLngBounds)しか受け付けません

円形で絞り込みたい場合は locationBias を使う必要があります(ただし厳密な制限ではなくバイアスになります)。

ハマりポイント②:center は LatLng オブジェクトが必要

{ lat: 35.68, lng: 139.76 } のようなプレーンオブジェクトを渡すと InvalidValueError になります。

// ❌ エラーになる
center: { lat: 35.68, lng: 139.76 }

// ✅ 正しい
center: new google.maps.LatLng(35.68, 139.76)

ハマりポイント③:isOpen() が関数じゃない場合がある

検索結果の regularOpeningHoursisOpen が関数として存在しないケースがありました。openNow プロパティをフォールバックとして使います。

getOpenNow(oh) {
  if (!oh) return null;
  try {
    if (typeof oh.isOpen === 'function') return oh.isOpen();
    if (oh.openNow != null) return oh.openNow;
  } catch {}
  return null;
},

ポイント②:APIキーをGitHub Secretsで管理

Vanilla JSのみの静的サイトなのでサーバーがなく、APIキーをどう扱うかが課題でした。

解決策:GitHub Actionsでビルド時に注入

config.js にはプレースホルダーを置いておき、デプロイ時に sed で置換します。

# .github/workflows/deploy.yml
- name: Inject API Keys
  run: |
    sed -i "s|GOOGLE_MAPS_API_KEY_PLACEHOLDER|${{ secrets.GOOGLE_MAPS_API_KEY }}|g" js/config.js
    sed -i "s|ADSENSE_CLIENT_PLACEHOLDER|${{ secrets.ADSENSE_CLIENT }}|g" index.html
    sed -i "s|ADSENSE_SLOT_PLACEHOLDER|${{ secrets.ADSENSE_SLOT }}|g" index.html

これでコードにAPIキーを含めずに済みます。あわせてGoogle Cloud ConsoleでAPIキーのHTTPリファラー制限を設定して、自分のドメイン以外からの使用をブロックしています。


ポイント③:iOS Safari の位置情報対応

iOSではユーザーの操作なしに getCurrentPosition() を呼ぶとブロックされることがあるため、位置情報エリア全体をタップ可能なボタンにしました。

<div class="location-row" id="location-row" role="button" tabindex="0">
  <div id="location-dot" class="location-dot"></div>
  <span id="location-status-text">📍 タップして現在地を取得</span>
</div>
document.getElementById('location-row')
  .addEventListener('click', () => this.getLocation());

タップ(ユーザーのジェスチャー)の中で getCurrentPosition() を呼ぶことで、iOSでも正常に動作します。


ポイント④:AdSenseを display:none の要素に適用しない

AdSenseの初期化を非表示の画面に対して行うと No slot size for availableWidth=0 エラーが発生します。

showScreen() 内で、画面を表示してから100ms後に広告を初期化することで解決しました。

showScreen(name) {
  document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
  document.getElementById(`${name}-screen`).classList.add('active');

  // 表示後に広告を初期化
  setTimeout(() => {
    const screen = document.getElementById(`${name}-screen`);
    screen.querySelectorAll('ins.adsbygoogle:not([data-adsbygoogle-status])').forEach(() => {
      try { (adsbygoogle = window.adsbygoogle || []).push({}); } catch (e) {}
    });
  }, 100);
},

ポイント⑤:ルーレットアニメーション

3枚のカードが順番に「スロットマシン風」に出現します。

spinCard(index, restaurant) {
  const card = document.getElementById(`roulette-card-${index}`);
  const allNames = this.searchResults.map(r => r.displayName);

  card.className = 'roulette-card spinning';

  // 高速で店名を切り替え(スロットマシン演出)
  const interval = setInterval(() => {
    const fake = allNames[Math.floor(Math.random() * allNames.length)];
    card.querySelector('.roulette-card-content').innerHTML =
      `<div class="card-name" style="opacity:0.6">${fake}</div>`;
  }, 120);

  // 1.2秒後に停止して本当の店名を表示
  setTimeout(() => {
    clearInterval(interval);
    card.className = 'roulette-card revealed clickable';
    card.querySelector('.roulette-card-content').innerHTML = this.buildCardHTML(restaurant);
    card.onclick = () => this.selectCard(index, restaurant, card);
  }, 1200);
},

検索結果の全店名を「フェイク候補」として使うので、実際に近くにある店名が流れるのがリアルで面白いです。


デプロイ方法

必要なもの

  1. Google Cloud Console でAPIキーを取得(Maps JavaScript API + Places API (New) を有効化)
  2. GitHub リポジトリを作成
  3. GitHub Secrets に以下を登録:
Secret名
GOOGLE_MAPS_API_KEY Google Maps APIキー
ADSENSE_CLIENT ca-pub-XXXXXXXXXX(AdSense申請後)
ADSENSE_SLOT 広告ユニットID(AdSense申請後)
  1. Settings → Pages → Source: GitHub Actions

pushするだけで自動デプロイ

git push origin main
# → GitHub Actions が自動でAPIキーを注入してデプロイ

まとめ

課題 解決策
APIキーをコードに含めたくない GitHub Actions + Secrets で注入
iOS Safariで位置情報が拒否される タップ操作で getCurrentPosition() を呼ぶ
isOpen() が動かない openNow プロパティをフォールバック利用
AdSenseが非表示要素でエラー showScreen() 後に100msディレイして初期化
searchByTextlocationRestriction エラー locationBias を使う

フレームワークなしの素のJavaScriptでも、Google Places APIを使えば実用的なアプリが作れました。

ぜひ試してみてください!→ https://arupaka610.github.io/lunch-roulette/


参考リンク

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?