Next.jsで「福岡ルーレット」を作った【SVGマップ × ランダム抽選】

next.js

※プロモーションページが含まれる場合があります

Next.jsで「福岡ルーレット」を作った話【SVGマップ × ランダム抽選】

この記事でわかること

「次の旅行、どこ行く?」をルーレットで決めるWebアプリを作りました。

  • 福岡県の地図をSVGで表示
  • クリックした市町村の情報を表示
  • ルーレットでランダム抽選&ミッション付与
  • Next.js 15 + TypeScript + Tailwind CSSで実装

デモ: 実際に動かせます

fukuoka-game.mutsukichi-cooldciel.com

なぜ作ったのか

福岡県民歴○年ですが、行ったことない市町村が意外と多いことに気づきました。

「どこ行こう?」と悩むより、ルーレットで決めちゃえば行動しやすいかもと思って作成。

ついでに「その場所で〇〇をする」みたいなミッションも出すようにして、ゲーム性を持たせました。

システムの全体像

主な機能

  1. 福岡県の市町村マップ(SVG)
  • クリックで市町村情報を表示
  1. スロットルーレット
  • 60市町村からランダム抽選
  • アニメーション付き
  1. アクセス解析(Google Analytics)
  • どのくらい遊ばれているかを計測

技術スタック

  • Next.js 15 (App Router)
  • TypeScript
  • Tailwind CSS
  • React Hooks (カスタムフック)
  • SVG (地図データ)

ディレクトリ構成

src/
├── app/
│   ├── layout.tsx          # ルートレイアウト(GA設定も含む)
│   ├── page.tsx            # メインページ
│   └── globals.css         # グローバルスタイル
├── components/
│   ├── FukuokaMap.tsx      # SVGマップコンポーネント
│   ├── SlotRoulette.tsx    # ルーレットコンポーネント
│   ├── MissionBoard.tsx    # ミッション表示
│   └── GoogleAnalytics.tsx # GA4設定
├── data/
│   ├── fukuokaLocations.ts # 市町村データ
│   └── fukuokaMapPaths.ts  # SVGパスデータ
├── hooks/
│   └── useGameState.ts     # ゲーム状態管理
└── types/
    └── index.ts            # 型定義

実装のポイント

① SVGマップの作成

福岡県の地図をSVGで実装するのが一番大変でした。

src/data/fukuokaMapPaths.ts

export const fukuokaMapPaths = {
  福岡市: "M 150,200 L 180,210 L 190,230 ...",
  北九州市: "M 50,50 L 80,60 L 90,80 ...",
  // ...60市町村分のSVGパス
}

詰まったポイント

  • 最初はGoogle Maps APIを使おうとした → 使用料が高い
  • 国土地理院のデータを使った → SVG変換が面倒
  • 結局、地図データをトレースしてパスを手動作成

時間はかかったけど、これで完全に自由にカスタマイズできるようになりました。

src/components/FukuokaMap.tsx(抜粋)

export default function FukuokaMap({ onLocationSelect }: Props) {
  return (
    <svg viewBox="0 0 800 600" className="w-full h-auto">
      {Object.entries(fukuokaMapPaths).map(([location, path]) => (
        <path
          key={location}
          d={path}
          fill="currentColor"
          className="hover:fill-blue-400 cursor-pointer transition-colors"
          onClick={() => onLocationSelect(location)}
        />
      ))}
    </svg>
  )
}

② ルーレットアニメーション

スロットマシン風のアニメーションを実装。

src/components/SlotRoulette.tsx(抜粋)

const [spinning, setSpinning] = useState(false)
const [result, setResult] = useState<Location | null>(null)

const spin = () => {
  setSpinning(true)

  // アニメーション中、高速で市町村を切り替え
  const interval = setInterval(() => {
    setResult(locations[Math.floor(Math.random() * locations.length)])
  }, 50)

  // 3秒後に停止
  setTimeout(() => {
    clearInterval(interval)
    const finalResult = locations[Math.floor(Math.random() * locations.length)]
    setResult(finalResult)
    setSpinning(false)
  }, 3000)
}

工夫したところ

  • 停止する瞬間をゆっくりにして、期待感を演出
  • アニメーション中はボタンを無効化
  • 結果表示後、地図上でハイライト

③ ミッション生成ロジック

ランダムなミッションを生成して、旅を面白くします。

src/hooks/useGameState.ts(抜粋)

const missions = [
  "地元の名物を食べる",
  "景色の良い場所で写真を撮る",
  "地元の人と会話する",
  "隠れた名所を見つける",
  "お土産を買う"
]

const generateMission = (location: Location) => {
  const mission = missions[Math.floor(Math.random() * missions.length)]
  return `${location.name}で${mission}`
}

④ 市町村データの構造

60市町村の情報を管理。

src/data/fukuokaLocations.ts(抜粋)

export const fukuokaLocations: Location[] = [
  {
    id: "fukuoka-city",
    name: "福岡市",
    description: "福岡県の県庁所在地",
    feature: "博多ラーメン、博多祇園山笠",
    population: 1600000,
  },
  // ...
]

⑤ Google Analyticsの統合

デプロイ後、どのくらい遊ばれているか知りたいと思い、GA4を導入しました。

Google Analytics実装手順

① Google Analyticsコンポーネントを作る

まず、GA4のトラッキングコードを独立したコンポーネントにします。

src/components/GoogleAnalytics.tsxを作成

import Script from 'next/script'

export default function GoogleAnalytics() {
  const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID

  if (!GA_MEASUREMENT_ID) {
    return null
  }

  return (
    <>
      <Script
        strategy="afterInteractive"
   開発中に詰まったこと

### SVGパスの座標がずれる問題

最初、地図の形が**全然合わない**。

原因:**viewBoxの設定ミス**

tsx
// ❌ これだとずれる

// ⭕ 実際の座標範囲に合わせる

### ルーレットが同じ場所を選びすぎる

完全にランダムなのに、なぜか**福岡市ばかり出る**気がする...

実装を見直したら、本当にただのランダムでした。
人間の脳は**偏りを感じやすい**だけでした(笑)

### スマホで地図が小さすぎる

レスポンシブ対応を後回しにしたら、**スマホで全然使えない**状態に。

解決策:**Tailwindのクラスで調整**

tsx

今後の改善予定

- [ ] 訪問済みの場所を記録(localStorage)
- [ ] SNSシェア機能
- [ ] 市町村の詳細情報ページ
- [ ] 複数人でプレイできるモード
- [ ] ルート検索(抽選された場所への行き方)

## 技術選定の理由

### なぜNext.js?

- **App Router**で最新の構成を学びたかった
- Vercelに簡単にデプロイできる
- SSR/SSGの使い分けができる(今回は主にCSR)

### なぜTypeScript?

- 市町村データの型を安全に管理したかった
- **コンポーネントのpropsをしっかり定義**できる

typescript
interface Location {
id: string
name: string
description: string
fソースコード

GitHub: tokunaga3/fukuoka-game

参考リンク


**「自分の地域版も作ってみたい!」という方がいたら、コメントやDMで教えてください

  • カスタムCSSを書く量が減る

学んだこと

SVGは意外と扱いやすい

最初は「難しそう…」と思ったけど、pathとfillだけでかなり表現できます。

地図以外にも、アイコンやグラフに使えそう。

アニメーションは体験を変える

ルーレットが止まる瞬間のワクワク感が、システムの魅力になりました。

技術的には単純だけど、ユーザー体験への影響は大きい

完璧を目指さず公開する

「全市町村の詳細データを入れてから…」と思ったけど、70%で公開しました。

結果、早くフィードバックをもらえたのが良かったです。

まとめ

「福岡ルーレット」は:

  • Next.js + TypeScript + SVGで実装
  • ルーレットでランダム抽選&ミッション付与
  • Google Analyticsでアクセス解析

個人開発は作りながら学ぶのが一番楽しいです。

「旅行の行き先を決めるアプリ」というアイデアは、他の都道府県でも使えそうなので、テンプレート化も視野に入れています
{children}


);
}

#### ここで詰まったポイント

最初、`<head>`に直接書こうとして**エラー**になりました。

App Routerでは`metadata`オブジェクトを使うか、`next/script`を使う必要があります。

### ③ 環境変数を設定する

**`.env.local`を作成(ルートディレクトリ)**

bash
NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
NEXT_PUBLIC_GSC_VERIFICATION=xxxxxxxxxxxxx

#### 注意点

- `NEXT_PUBLIC_`プレフィックスが必須(クライアント側で使うため)
- `.env.local`は`.gitignore`に含まれている(リポジトリにコミットされない)

**`.env.local.example`も作っておく**

bash

Google Analytics 4 測定ID

GA4のプロパティから取得 (例: G-XXXXXXXXXX)

NEXT_PUBLIC_GA_MEASUREMENT_ID=

Google Search Console 認証用メタタグ (オプション)

Search Consoleの所有権確認から取得

NEXT_PUBLIC_GSC_VERIFICATION=

### ④ Google Analyticsで測定IDを取得

1. [Google Analytics](https://analytics.google.com/)にログイン
2. 「管理」→「プロパティを作成」
3. プロパティ名を入力(例:福岡ルーレット)
4. 「データストリーム」→「ウェブ」を選択
5. サイトURLを入力
6. **測定ID(G-XXXXXXXXXX)**をコピー
7. `.env.local`に貼り付け

### ⑤ Search Consoleで所有権確認(オプション)

1. [Google Search Console](https://search.google.com/search-console)にアクセス
2. 「プロパティを追加」→「URLプレフィックス」でサイトURLを入力
3. 確認方法で「HTMLタグ」を選択
4. `content="..."`の値をコピー
5. `.env.local`に貼り付け

### ⑥ 開発サーバーを再起動

bash
npm run dev

環境変数を変更したときは**必ず再起動**が必要です。

## 確認方法

### ローカル環境で確認

1. ブラウザのデベロッパーツールを開く
2. **Network**タブを見る
3. `gtag/js`や`collect`というリクエストがあればOK

### 本番環境で確認

1. Vercelなどにデプロイ
2. 環境変数を設定(Vercelの「Settings」→「Environment Variables」)
3. Google Analyticsのリアルタイムレポートを開く
4. 自分でサイトにアクセス
5. **リアルタイムユーザーが1人増えればOK**

## うまくいかなかったこと

### 最初は`<Script>`を`metadata`に書こうとした

tsx
// ❌ これは動かない
export const metadata = {
title: “…”,
script: [{ src: “…”, strategy: “afterInteractive” }]
}

App Routerでは`metadata`に`script`は書けません。
`next/script`コンポーネントを使う必要があります。

### 環境変数が読み込まれない

開発サーバーを再起動し忘れていました。
`.env.local`を変更したら**必ず再起動**。

### 開発環境でもトラッキングされてしまう

最初は環境変数チェックを入れていなかったので、自分のアクセスもカウントされていました。

解決策:**環境変数がある場合のみコンポーネントを表示**

tsx
if (!GA_MEASUREMENT_ID) {
return null
}
“`

開発環境では.env.localに値を入れないようにすれば、トラッキングされません。

メリット・使いどころ

メリット

  • どのページがよく見られているかわかる
  • 滞在時間、離脱率、流入元が見える
  • 個人開発でも無料で使える

こんな時に便利

  • 「作ったけど誰も見てない…」を可視化したい
  • SNSで拡散したときの効果を測りたい
  • リリース後の改善点を見つけたい

まとめ

Next.js App RouterでGA4を実装するには:

  1. GoogleAnalytics.tsxコンポーネントを作る
  2. layout.tsxに追加する
  3. 環境変数で測定IDを管理
  4. 本番環境にデプロイして確認

完璧じゃなくてもOKです。
まずはリアルタイムレポートで1カウントされることを確認してみてください。

参考リンク


コメント

タイトルとURLをコピーしました