尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證人気のガチャガチャを探して、設置場所や在庫情報をチェックできるガチャ検索・マップアプリ。
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
欲しいガチャを見つける。残っている場所まで分かる。 ガチャガチャの検索と、設置場所・在庫状況の確認ができる Web アプリです。 データは モックデータ(既定) と Cloud Firestore を環境変数で切り替えられます。 Firebase プロジェクトが無くても、モックデータのまま開発・ビルドできます。 • Next.js 16(App Router) • TypeScript • Tailwind CSS v4 • Firebase(Cloud Firestore / Admin SDK)※ Spark プラン(無料枠)前提 • スマートフォン優先のレスポンシブデザイン Node.js 20.6 以上が必要です。 環境変数を設定しない場合は、モックデータ(DATA_SOURCE=mock)で動作します。檢視完整 README ↗
欲しいガチャを見つける。残っている場所まで分かる。
ガチャガチャの検索と、設置場所・在庫状況の確認ができる Web アプリです。 データは モックデータ(既定) と Cloud Firestore を環境変数で切り替えられます。 Firebase プロジェクトが無くても、モックデータのまま開発・ビルドできます。
Node.js 20.6 以上が必要です。
npm install # 依存パッケージのインストール
npm run dev # 開発サーバー起動 → http://localhost:3000
その他のコマンド:
npm run build # 本番ビルド
npm run start # 本番ビルドの起動
npm run lint # ESLint
npm run typecheck # 型チェック
npm test # テスト(Firestore には接続しない。実データは DATA_SOURCE=local で読み込む)
npm run catalog:build # 収集データから投入予定の内容・カタログ索引をローカルに出力(Firestore には接続しない)
npm run firestore:check # Firestore への接続確認(読み取りのみ)
npm run auth:check # 匿名ログインの動作確認(確認用の匿名ユーザーは自動で削除)
npm run seed:firestore # モックデータを Firestore に投入(Firebase 設定後)
環境変数を設定しない場合は、モックデータ(DATA_SOURCE=mock)で動作します。
| DATA_SOURCE | データ | 在庫報告 |
|---|---|---|
mock(既定) | src/data/mock.ts の架空データ | 保存しない |
local | 収集した実データ data/collected/*.json(約2.3万商品・1,800店舗)。Firestore に接続しない | サーバーのメモリ上のみ |
firestore | Cloud Firestore | 保存する |
DATA_SOURCE=local npm run dev # 実データ規模で画面・検索を確認(Firestore 投入前の確認用)
| パス | 内容 |
|---|---|
/ | トップ。「欲しいガチャを探そう」+検索バー、🔥話題のガチャ / 🆕新着ガチャ / 📍近くで見つかったガチャ |
/search?q=キーワード | 検索結果(画像・商品名・シリーズ・メーカー・価格・設置店舗数・在庫あり報告店舗数)。「在庫ありのみ」フィルタ付き |
/gacha/[id] | ガチャ詳細(画像・商品名・シリーズ・メーカー・価格・発売時期)、「この店で見つけた」報告、「このガチャが見つかった場所」(地図+リスト) |
/locations/[id]?product=[商品ID] | 設置場所詳細(店舗名・住所・営業時間・地図)、対象ガチャの在庫状態・最終確認日時、在庫報告ボタン |
DATA_SOURCE=firestore のときは Firestore に保存され、
mock のときは保存されず画面表示にのみ反映されます(local はサーバーのメモリ上のみ)。src/components/stock/FoundAtStoreReport.tsx)。
店舗の検索は店舗の索引だけで行い、locations は読みません(src/app/actions/locations.ts)。src/
├── app/ # ルーティング(App Router)
│ ├── page.tsx # トップページ
│ ├── search/page.tsx # 検索結果
│ ├── gacha/[id]/page.tsx # ガチャ詳細
│ ├── locations/[id]/page.tsx # 設置場所詳細
│ ├── actions/stockReports.ts # 在庫報告の Server Action
│ └── layout.tsx / globals.css / not-found.tsx
├── components/ # UI コンポーネント(layout / search / gacha / location / stock / map / ui)
├── data/
│ └── mock.ts # ★ モックデータはすべてここに集約(Firestore への初期投入にも使用)
├── lib/
│ ├── data/ # データアクセス層
│ │ ├── source.ts # DataSource インターフェース
│ │ ├── config.ts # DATA_SOURCE による切り替え
│ │ ├── index.ts # 画面から使うクエリ関数(検索・集計)
│ │ ├── mockSource.ts # モック実装
│ │ ├── firestoreSource.ts # Firestore 実装(キャッシュ付き)
│ │ ├── firestore/schema.ts # Firestore のコレクション構造と型変換
│ │ ├── cacheTags.ts # キャッシュのタグ・保持時間
│ │ └── reports.ts # 在庫報告の送信(クライアント → Server Action)
│ ├── firebase/
│ │ ├── adminApp.ts / admin.ts # Admin SDK(サーバー専用)
│ │ └── client.ts # Client SDK(将来のログイン用・現在未使用)
│ ├── auth/reporter.ts # 報告者ID(匿名認証の ID トークンをサーバーで検証して uid を返す)
│ └── format.ts / geo.ts / search.ts / stock.ts
└── types/index.ts # ドメインモデルの型定義
scripts/seed-firestore.ts # モックデータの Firestore 投入スクリプト
scripts/check-firestore.ts # Firestore 接続確認スクリプト
firebase.json # Firebase CLI 設定
firestore.rules # Firestore Security Rules
firestore.indexes.json # Firestore インデックス
storage.rules # Storage Security Rules(現在は未使用)
public/images/gacha/ # 商品画像(SVG)
「ガチャ商品」と「設置場所」は別エンティティとして扱い、多対多の関係を Placement で表します。
GachaProduct 1 ── * Placement * ── 1 Location
│ latestStock(最新在庫のキャッシュ)
│
GachaProduct × Location ── * StockReport(履歴) * ── 1 User
| 型 | 役割 |
|---|---|
GachaProduct | ガチャ商品(名前・シリーズ・メーカー・価格・発売時期・画像など) |
Location | 設置場所(店舗名・住所・緯度経度・営業時間) |
Placement | どの商品がどの場所に設置されているか |
StockReport | ユーザーによる在庫報告(在庫あり / 残りわずか / 売り切れ + 報告日時) |
User | 報告したユーザー |
「在庫あり報告店舗数」は、最新報告が「在庫あり」または「残りわずか」の店舗数です。
| コレクション | ドキュメントID | 主なフィールド |
|---|---|---|
products | 任意(モックは p-001 など) | name, series, maker, price, releaseMonth, imageUrl, description, characters, tags |
locations | 任意(モックは l-001 など) | name, address, area, lat, lng, openingHours |
placements | {productId}__{locationId} | productId, locationId, firstSeenAt, latestStock(status, reportedAt, reportId / 報告なしは null) |
stockReports | 自動ID | productId, locationId, placementId, userId(報告者の Firebase Auth uid), status, reportedAt |
users | Firebase Auth の uid | displayName, createdAt(匿名ユーザーは初回報告時に「ゲスト」として作成) |
users/{uid}/reportThrottles | {productId}__{locationId} | lastReportedAt(連投制限用) |
stockReports は履歴としてすべて残し、更新・削除しません。stockReports への追加」と「placements.latestStock の更新」をまとめて行います。
既存の latestStock より 新しい報告のときだけ 上書きするため、古い報告で最新状態が戻ることはありません。placements.latestStock だけを読み、報告履歴は読みません(読み取り件数の削減)。verifyIdToken で検証した uid を報告者として保存します。REPORT_COOLDOWN_MS・src/lib/data/source.ts)。判定は報告保存と同じトランザクション内で行います。guest のもの)はそのまま残ります。Firestore の無料枠(1日あたり 読み取り 50,000 件 / 書き込み 20,000 件、保存 1GiB)に収まるよう、以下の構成にしています。
catalogIndex コレクション。商品名・メーカー・価格・発売月など
一覧に必要な項目だけを 700KB 以下のドキュメントに分割したもの)だけで行います。約2.3万商品で 12 シャード+店舗 1 シャード。
索引の読み込みは meta を含め 14 読み取りで、1時間キャッシュします(シャードごとに Next.js のデータキャッシュ 2MB 制限内)。
商品詳細は 1 ドキュメントだけ読みます。索引に無い商品(モックなど)は一覧・検索に出ません。
索引がまだ無い場合(実データ投入前)は、従来どおり products / locations を読みます(最大 300 件)public/images/ に同梱しています(下記参照)利用者やデータが増えた場合も、同じ Firebase プロジェクトのまま Blaze プランへ切り替えるだけで継続でき、コードの変更は不要です。 設置情報(placements)が数千件を超える規模になったら、商品ごとの集計値を非正規化するなどを検討します。
商品の発売状況は保存せず、表示時に「発売月と再発売月の新しい方」から求めます(src/lib/release.ts)。
メーカー公式サイトの画像は、各社の利用規約(無断転載禁止など)の確認が済むまで表示しません(画像なしのプレースホルダーを表示)。
権利確認が済んだホストは NEXT_PUBLIC_APPROVED_IMAGE_HOSTS(カンマ区切り)に指定すると表示されます(src/lib/images.ts)。
GachaNavi オリジナルのアイコン(水色のガチャカプセル+青いナビ矢印・軌道+黄色のマップピン)を使います。
元データは assets/brand/(正式デザインの画像・小サイズ用の簡略版 SVG・通知バッジ用の単色 SVG)で、
node scripts/build-icons.mjs で favicon(src/app/favicon.ico)・PWA・Apple Touch Icon・通知アイコン・バッジ・ヘッダーのロゴ
(public/icons/)を作ります。外部の画像・メーカーやキャラクターの画像は使いません。
本番の Firestore に入っている架空のモックデータ(src/data/mock.ts の商品 10 件・店舗 8 件)は削除しません。
実データの投入スクリプトが、投入の最初にこれらの ID のドキュメントへ isSample: true を付けます(ID を指定して読むだけで全件は読みません)。
isSample: true の商品・店舗は、一覧・検索・商品詳細・店舗詳細・「この店で見つけた」の対象外になります(詳細は 404)data/collected/validation/REPORT.md の分類。要確認のデータも削除せず、現時点ではすべて投入対象です(除外推奨は 0 件)。
| 問題なし | 修正可能 | 要確認 | 除外推奨 | 合計 | |
|---|---|---|---|---|---|
| 商品 | 21,880 | 970 | 614 | 0 | 23,464 |
| 店舗 | 1,373 | 203 | 220 | 0 | 1,796 |
src/lib/catalog/collected.ts)で対応済み2024年10月以降に作成された Firebase プロジェクトでは、Cloud Storage for Firebase の利用に Blaze プランが必要です。
そのため現時点では Storage を使わず、商品画像は public/images/gacha/ に置いています。
GachaProduct.imageUrl には任意の URL を保存できるため、将来 Storage を使う場合も画像 URL を差し替えるだけで対応できます
(その際は next.config.ts の images.remotePatterns に Storage のドメインを追加し、storage.rules をデプロイします)。
Firebase プロジェクト作成後に、以下の手順で Firestore に接続します。
gachanavi)。Google アナリティクスは不要なら無効で構いませんasia-northeast1 東京 / asia-northeast2 大阪)を推奨(作成後は変更できません。
アプリ側でロケーションを指定する設定は不要です)firestore.rules を後でデプロイします).env.local を作成するcp .env.example .env.local
ダウンロードした JSON の値を設定します。
DATA_SOURCE=firestore
FIREBASE_PROJECT_ID=(JSON の project_id)
FIREBASE_CLIENT_EMAIL=(JSON の client_email)
FIREBASE_PRIVATE_KEY="(JSON の private_key。\n を含む1行のまま "" で囲む)"
在庫報告の匿名認証に NEXT_PUBLIC_FIREBASE_* も必要です。「プロジェクトの設定」→「全般」→「マイアプリ」で
ウェブアプリ(</>)を登録すると表示される firebaseConfig の値を設定します。
NEXT_PUBLIC_ の値はビルド時に埋め込まれるため、設定・変更後は npm run dev / npm run build をやり直してください。
さらに Firebase コンソールの「Authentication」→「始める」→「ログイン方法」で 匿名 を有効にしてください。 無効のままだと、在庫報告時に「報告者の確認ができませんでした」と表示され保存されません。
.env.local の代わりに、シェルやホスティング先(Vercel など)・クラウド開発環境の設定画面で
同じ名前の環境変数を設定しても動作します(Next.js もスクリプトも両方に対応しています)。
npm run firestore:check # 接続確認(各コレクションの件数を表示。書き込みはしない)
npm run auth:check # 匿名ログイン → サーバーでの ID トークン検証まで確認
npm run seed:firestore # モックデータ(約80件)を Firestore に書き込み
npm run dev
アプリは Admin SDK(Security Rules の対象外)でアクセスするため、この手順を行わなくても動作します。
「本番環境モード」で作成した場合、初期ルールはブラウザからのアクセスをすべて拒否する内容で、
firestore.rules とほぼ同じです。ルールをリポジトリの内容と揃える場合や、在庫報告の履歴を表示する機能
(複合インデックスが必要)を追加する際に実行してください。
npx firebase-tools login
npx firebase-tools use --add # 作成したプロジェクトを選択(.firebaserc が作成されます)
npx firebase-tools deploy --only firestore:rules,firestore:indexes
在庫報告ボタンを押すと、Firestore の stockReports に履歴が追加され、placements.latestStock が更新されます。
Firebase プロジェクトの無料枠を消費せずに試せます(Java が必要です)。
npx firebase-tools emulators:start --only auth,firestore --project demo-gachanavi
.env.local に DATA_SOURCE=firestore、FIREBASE_PROJECT_ID=demo-gachanavi、
FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 を設定すると、サービスアカウント無しでエミュレータに接続します。
匿名認証もエミュレータで試す場合は、FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099、
NEXT_PUBLIC_FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099、NEXT_PUBLIC_FIREBASE_PROJECT_ID=demo-gachanavi、
NEXT_PUBLIC_FIREBASE_API_KEY と NEXT_PUBLIC_FIREBASE_APP_ID(エミュレータでは任意の文字列で可)も設定します。
収集データ(data/collected)の投入処理は、本番に投入する前に Firestore Emulator で確認します。
本番 Firestore には接続しません(scripts/lib/emulatorGuard.ts が、FIRESTORE_EMULATOR_HOST がローカルであること・
プロジェクト ID が demo- で始まることを確認し、本番用の認証情報の環境変数をプロセス内で消去します)。
# 投入処理の統合テスト(エミュレータを起動 → テスト → 停止。Java が必要)
npm run test:emulator
# 手動で試す場合
npm run emulators # 別ターミナル
FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 npm run import:emulator -- --dry-run
FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 npm run import:emulator -- --max-writes=15000
catalogIndex/meta(最後)。meta が書かれるまでアプリは新しい索引を使いません.catalog-build/)で、変わっていないドキュメントは書き直しません--max-writes で 1 回の書き込み件数を制限できます(無料枠 20,000 件/日。上限で止まったら翌日に同じコマンドで続きから)画面の確認は、本番用の認証情報を外してエミュレータに向けたアプリを起動し、test:emulator:app を実行します
(投入テスト後のデータが前提のため、npm run emulators で起動したエミュレータに対して投入テストを実行しておく)。
FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099 \
npx tsx --conditions=react-server --test tests/emulator/import.test.ts
env -u FIREBASE_PRIVATE_KEY -u FIREBASE_CLIENT_EMAIL DATA_SOURCE=firestore FIREBASE_PROJECT_ID=demo-gachanavi \
FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099 sh -c 'npm run build && npx next start -p 3100'
GACHANAVI_APP_URL=http://127.0.0.1:3100 npm run test:emulator:app
本番投入は --target=production で行います(エミュレータ用のガードはそのまま。本番は scripts/lib/productionGuard.ts で別に確認)。
次のすべてを満たさない場合は、接続する前に停止します。
--project=gachanavi-21ee8(コードに固定したプロジェクト ID と完全一致)と、環境変数 FIREBASE_PROJECT_ID の一致FIREBASE_CLIENT_EMAIL がこのプロジェクトのサービスアカウント、FIREBASE_PRIVATE_KEY が設定済みFIRESTORE_EMULATOR_HOST などエミュレータ用の環境変数が未設定--max-writes=(1〜20,000)の指定と、書き込み時はドライランで表示される確認コード --confirm-plan=# 1) ドライラン(書き込みなし。Firestore の既存 ID を確認するため既存ドキュメント数だけ読み取り)
npx tsx scripts/import-collected.ts --target=production --project=gachanavi-21ee8 --max-writes=15000 --dry-run
# 2) 書き込み(1 日 1 回。上限で停止したら、翌日に同じコマンドで続きから)
npx tsx scripts/import-collected.ts --target=production --project=gachanavi-21ee8 --max-writes=15000 --confirm-plan=<確認コード>
create())。再開は Firestore の既存 ID で判定し、未投入の ID だけを書きます(ローカルの記録に依存しない)--max-writes はモックの isSample 更新(18 件)と meta を含む上限ですcatalogIndex/meta を書きます。既存の索引と内容が異なる場合は何も書かずに停止しますお気に入りの「在庫報告があったら通知」が ON のとき、GachaNavi を閉じていても在庫あり・残りわずかの報告を通知します。
Spark(無料)プランのまま、Cloud Functions を使わずに動きます(送信は Vercel の Server Action → Firebase Admin SDK → FCM)。
詳細は docs/favorites-and-stock-alerts.md。
有効にする手順(本番で行う前に内容を確認すること):
NEXT_PUBLIC_FIREBASE_VAPID_KEY に設定(公開してよい値。秘密鍵はコンソールから出さない)npx firebase-tools deploy --only firestore:indexes --project gachanavi-21ee8
(collection group favorites:productId + notifyInStock。既存のデータ・Rules は変わらない。インデックスの作成も無料)NEXT_PUBLIC_* はビルド時に埋め込まれるため)NEXT_PUBLIC_FIREBASE_VAPID_KEY が未設定、またはインデックスが未作成の間は、バックグラウンド通知は送られず、
従来どおり GachaNavi を開いている間のお知らせだけになる(在庫報告・お気に入りには影響しない)Next.js のサーバー機能(Server Action・キャッシュ)を使うため、サーバーを実行できる環境が必要です。
Firebase App Hosting は Blaze プランが必要なため、Spark プランのうちは Vercel(Hobby プラン)などを想定しています。
デプロイ先の環境変数に .env.local と同じ値を設定してください。
firebase-admin は 13 系に固定しています。14 系が依存する jwks-rsa@4 は ESM 専用の jose@6 を
require() で読み込むため、require(esm) に対応していない Node.js で実行すると
ERR_REQUIRE_ESM(Failed to load external module firebase-admin/auth)となり全ページが 500 になります
(Vercel 本番環境で発生)。14 系に上げる場合は、実行環境の Node.js が require(esm) に対応しているか
(node --no-experimental-require-module で起動して再現しないか)を確認してください。
商品・店舗ページの title / description / canonical / OGP / JSON-LD / パンくず、/robots.txt、/sitemap.xml
(サイトマップ インデックス)を出力します。検索エンジンに公開するのは DATA_SOURCE=firestore かつ Vercel 本番
(VERCEL_ENV=production)のときだけで、Preview・開発・モックは noindex と Disallow: / になります。
本番の公開 URL は NEXT_PUBLIC_SITE_URL で指定します。仕様・確認結果・Search Console での作業は
docs/seo-report.md を参照してください。
LocationMapProps(src/components/map/types.ts)を満たす実装を作り、
src/components/map/LocationMap.tsx で切り替えます。表示している商品名・メーカー・店舗名・住所・在庫情報はすべて架空のダミーデータです。