Next.jsで「福岡ルーレット」を作った話【SVGマップ × ランダム抽選】
この記事でわかること
「次の旅行、どこ行く?」をルーレットで決めるWebアプリを作りました。
- 福岡県の地図をSVGで表示
- クリックした市町村の情報を表示
- ルーレットでランダム抽選&ミッション付与
- Next.js 15 + TypeScript + Tailwind CSSで実装
デモ: 実際に動かせます
fukuoka-game.mutsukichi-cooldciel.com
なぜ作ったのか
福岡県民歴○年ですが、行ったことない市町村が意外と多いことに気づきました。
「どこ行こう?」と悩むより、ルーレットで決めちゃえば行動しやすいかもと思って作成。
ついでに「その場所で〇〇をする」みたいなミッションも出すようにして、ゲーム性を持たせました。
システムの全体像
主な機能
- 福岡県の市町村マップ(SVG)
- クリックで市町村情報を表示
- スロットルーレット
- 60市町村からランダム抽選
- アニメーション付き
- アクセス解析(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を実装するには:
GoogleAnalytics.tsxコンポーネントを作るlayout.tsxに追加する- 環境変数で測定IDを管理
- 本番環境にデプロイして確認
完璧じゃなくてもOKです。
まずはリアルタイムレポートで1カウントされることを確認してみてください。

コメント