diff --git a/.claude/docs/plugin/installed-plugins.md b/.claude/docs/plugin/installed-plugins.md index 2bf1ea0..98c21d8 100644 --- a/.claude/docs/plugin/installed-plugins.md +++ b/.claude/docs/plugin/installed-plugins.md @@ -31,4 +31,3 @@ | `commit-commands` | `smart-commit`(削除済み) | `/commit` で同等機能。CLAUDE.mdの日本語ルール優先 | | `code-review` | — | `/create-pr` と組み合わせて使う | | `frontend-design` | — | 既存の ui-ux-pro-max スキルと補完関係 | -| `typescript-lsp` / `ruby-lsp` | — | serena MCPと補完関係 | diff --git a/.claude/hooks/auto-lint.sh b/.claude/hooks/auto-lint.sh new file mode 100755 index 0000000..ed2b1c3 --- /dev/null +++ b/.claude/hooks/auto-lint.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +# PostToolUse hook: Edit/Write 後に変更ファイルのリンター・フォーマッターを自動実行 + +set -e + +INPUT=$(cat) +FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') + +if [ -z "$FILE" ] || [ "$FILE" = "null" ]; then + exit 0 +fi + +if [ ! -f "$FILE" ]; then + exit 0 +fi + +PROJECT_DIR="${CLAUDE_PROJECT_DIR:-/Users/shimizuippei/projects/dev/buzzbase}" + +# front/ +if [[ "$FILE" == *"/front/"* ]]; then + cd "$PROJECT_DIR/front" + npx prettier --write "$FILE" 2>/dev/null || true + npx eslint --fix "$FILE" 2>/dev/null || true + npx tsc --noEmit 2>/dev/null || true + exit 0 +fi + +# back/ (.rb のみ) +if [[ "$FILE" == *"/back/"* && "$FILE" == *.rb ]]; then + RELATIVE="${FILE#*back/}" + cd "$PROJECT_DIR" + docker compose exec -T back bundle exec rubocop -a "$RELATIVE" 2>/dev/null || true + exit 0 +fi + +# mobile/ +if [[ "$FILE" == *"/mobile/"* ]]; then + cd "$PROJECT_DIR/mobile" + npx prettier --write "$FILE" 2>/dev/null || true + npx eslint --fix "$FILE" 2>/dev/null || true + npx tsc --noEmit 2>/dev/null || true + exit 0 +fi + +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json index 74242c8..ba3ebda 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -35,16 +35,19 @@ "Bash(head *)", "Bash(tail *)", "Bash(grep *)", + "Bash(cd /Users/shimizuippei/projects/dev/buzzbase*)", "Bash(cd /Users/shimizuippei/projects/dev/buzzbase && *)", "Bash(cd /Users/shimizuippei/projects/dev/buzzbase/back && *)", + "Bash(cd /Users/shimizuippei/projects/dev/buzzbase/back*)", "Bash(cd /Users/shimizuippei/projects/dev/buzzbase/front && *)", + "Bash(cd /Users/shimizuippei/projects/dev/buzzbase/front*)", "Bash(cd /Users/shimizuippei/projects/dev/buzzbase/mobile && *)", + "Bash(cd /Users/shimizuippei/projects/dev/buzzbase/mobile*)", "Bash(ISSUE_NODE_ID=$(gh issue view * --repo ippei-shimizu/buzzbase *) && gh api graphql *)", "Bash(gh api graphql *)", "Bash(gh issue view *)", "Bash(gh issue list *)", - "Bash(ls -la * 2>/dev/null)", - "mcp__serena__*" + "Bash(ls -la * 2>/dev/null)" ], "deny": [ "Bash(curl *)", @@ -57,8 +60,21 @@ "Bash(git merge *)" ] }, + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/auto-lint.sh\"", + "timeout": 120 + } + ] + } + ] + }, "enabledPlugins": { - "superpowers@claude-plugins-official": true, "claude-md-management@claude-plugins-official": true, "feature-dev@claude-plugins-official": true, "commit-commands@claude-plugins-official": true, diff --git a/.claude/skills/README.md b/.claude/skills/README.md index 0026a9e..53634ba 100644 --- a/.claude/skills/README.md +++ b/.claude/skills/README.md @@ -79,97 +79,6 @@ issue番号からgit worktreeを作成。front/backサブモジュールのworkt --- -## gstack (外部プラグイン) - -Garry Tan製のClaude Codeエンジニアリングツールスイート。AIエージェントに組織的な役割を割り当て、開発プロセスを自動化する。 - -### 開発フロー - -``` -/office-hours → /plan-ceo-review → /plan-eng-review → 実装 → /review → /qa → /ship -``` - -### コマンド一覧 - -#### 企画・設計 - -| コマンド | 役割 | 内容 | -|----------|------|------| -| `/office-hours` | プロダクト思考家 | 要件を再考し実装案を生成 | -| `/plan-ceo-review` | CEO | スコープと製品ビジョンを評価 | -| `/plan-eng-review` | EM | アーキテクチャ設計・技術検証 | -| `/plan-design-review` | デザインレビュー | UIデザイン監査(レポートのみ) | -| `/design-review` | デザインレビュー | UIデザイン監査 + 修正ループ | -| `/design-consultation` | デザインコンサル | デザインシステムをゼロから構築 | -| `/autoplan` | 自動レビュー | CEO → デザイン → EMレビューを自動パイプライン実行 | - -#### 実装・レビュー - -| コマンド | 役割 | 内容 | -|----------|------|------| -| `/review` | スタッフエンジニア | コードレビューと自動修正 | -| `/cso` | セキュリティ責任者 | OWASP Top 10 / STRIDE脅威モデル分析 | -| `/investigate` | デバッガー | 体系的な根本原因分析 | - -#### テスト・QA - -| コマンド | 役割 | 内容 | -|----------|------|------| -| `/qa` | QAリード | ヘッドレスブラウザでテスト実行・バグ修正 | -| `/qa-only` | QAリード | テスト実行・レポートのみ(修正なし) | -| `/browse` | QAエンジニア | ヘッドレスブラウザ操作(ページ遷移、スクショ等) | -| `/benchmark` | パフォーマンス | パフォーマンスリグレッション検出 | - -#### デプロイ・運用 - -| コマンド | 役割 | 内容 | -|----------|------|------| -| `/ship` | リリースエンジニア | テスト → PR作成 → デプロイ | -| `/land-and-deploy` | リリース | マージ → デプロイ → canary検証 | -| `/canary` | 監視 | デプロイ後の監視ループ | -| `/setup-deploy` | セットアップ | デプロイ設定の初期化 | -| `/document-release` | ドキュメント | リリース後のドキュメント更新 | - -#### ユーティリティ - -| コマンド | 内容 | -|----------|------| -| `/retro` | 振り返り・レトロスペクティブ | -| `/freeze` | デプロイフリーズ | -| `/unfreeze` | デプロイフリーズ解除 | -| `/careful` | 慎重モード有効化 | -| `/guard` | ガードレール有効化 | -| `/connect-chrome` | ブラウザ接続設定 | -| `/setup-browser-cookies` | ブラウザCookie設定 | -| `/gstack-upgrade` | gstack自体のアップデート | - -### 使い方の例 - -``` -# 新機能の企画から始める -/office-hours - -# コードレビューを依頼 -/review - -# ブラウザでQAテスト -/qa - -# セキュリティ監査 -/cso - -# PR作成からデプロイまで一気に -/ship -``` - -### 管理 - -- インストール先: `.claude/skills/gstack/`(プロジェクトローカル) -- アップデート: `/gstack-upgrade` -- `.gitignore` でリポジトリからは除外済み - ---- - ## 自動トリガー型スキル 以下のスキルは手動呼び出し不要。対象作業時に自動で適用される。 diff --git a/.claude/skills/app-release-notes/SKILL.md b/.claude/skills/app-release-notes/SKILL.md new file mode 100644 index 0000000..4a11b18 --- /dev/null +++ b/.claude/skills/app-release-notes/SKILL.md @@ -0,0 +1,86 @@ +--- +name: app-release-notes +description: App Store Connect / Google Playの「このバージョンの最新情報」リリースノートを自動生成する。mobile/app.jsonのexpo.versionを現在バージョンとし、前回バージョンとのGit差分からユーザー向けリリースノートを作成する。「リリースノートを作成して」「ストアの更新情報を書いて」「app-release-notes」などで起動する。 +--- + +# App Release Notes + +App Store Connect / Google Playストア向けのリリースノート(「このバージョンの最新情報」)を自動生成する。 + +## ワークフロー + +### 1. バージョン情報を取得 + +`mobile/app.json`を読み、`expo.version`を現在のリリースバージョンとして取得する。 + +### 2. 前回バージョンのコミットを特定 + +mobileサブモジュール内でバージョン更新コミットを検索する: + +```bash +cd mobile +git log --oneline --all --grep="<前回バージョン番号>" +``` + +バージョン番号はセマンティックバージョニング(例: 1.0.4 → 1.0.5)で管理されているため、現在バージョンの1つ前のマイナーバージョンを検索する。 + +### 3. 差分コミットを取得 + +前回バージョン更新コミットから現在バージョン更新コミットまでのコミット一覧を取得する: + +```bash +git log --oneline <前回バージョンコミットハッシュ>..<現在バージョンコミットハッシュ> +``` + +必要に応じてマージPRの詳細も確認する: + +```bash +gh pr view --repo ippei-shimizu/buzzbase_mobile +``` + +### 4. リリースノートを生成 + +コミットメッセージとPR内容を分析し、以下のカテゴリに分類して日本語のリリースノートを作成する。 + +#### カテゴリ分類ルール + +| コミットプレフィックス | カテゴリ | +|---|---| +| `Add:` | 新機能 | +| `Update:`, `Change:`, `Refactor:`, `Chore:` | 改善 | +| `Fix:` | バグ修正 | +| `Remove:` | 削除(内容に応じて改善に含めてもよい) | + +#### 除外するコミット + +- バージョン更新コミット(`バージョンを X から Y に更新`) +- マージコミット(`Merge pull request`) +- ビルド番号更新(`ビルド番号を更新`) +- READMEの更新 +- CI/CD設定の変更 +- lint/format修正のみのコミット + +#### 出力フォーマット + +``` +【新機能】 +・{ユーザーが理解できる機能説明} + +【改善】 +・{改善内容の説明} + +【バグ修正】 +・{修正内容の説明} +``` + +#### 文章作成ルール + +- ユーザー視点で書く(技術的な内部実装の詳細は書かない) +- 「〜しました。」で統一する +- 関連する複数コミットは1つの項目にまとめる +- カテゴリに該当するコミットがない場合、そのカテゴリは省略する +- 1項目は1行で簡潔に書く + +### 5. 出力 + +生成したリリースノートをそのままコピーして使えるようにプレーンテキストで出力する。 diff --git a/.claude/skills/baseline-ui/SKILL.md b/.claude/skills/baseline-ui/SKILL.md new file mode 100644 index 0000000..889e1f0 --- /dev/null +++ b/.claude/skills/baseline-ui/SKILL.md @@ -0,0 +1,85 @@ +--- +name: baseline-ui +description: Validates animation durations, enforces typography scale, checks component accessibility, and prevents layout anti-patterns in Tailwind CSS projects. Use when building UI components, reviewing CSS utilities, styling React views, or enforcing design consistency. +--- + +# Baseline UI + +Enforces an opinionated UI baseline to prevent AI-generated interface slop. + +## How to use + +- `/baseline-ui` + Apply these constraints to any UI work in this conversation. + +- `/baseline-ui ` + Review the file against all constraints below and output: + - violations (quote the exact line/snippet) + - why it matters (1 short sentence) + - a concrete fix (code-level suggestion) + +## Stack + +- MUST use Tailwind CSS defaults unless custom values already exist or are explicitly requested +- MUST use `motion/react` (formerly `framer-motion`) when JavaScript animation is required +- SHOULD use `tw-animate-css` for entrance and micro-animations in Tailwind CSS +- MUST use `cn` utility (`clsx` + `tailwind-merge`) for class logic + +## Components + +- MUST use accessible component primitives for anything with keyboard or focus behavior (`Base UI`, `React Aria`, `Radix`) +- MUST use the project’s existing component primitives first +- NEVER mix primitive systems within the same interaction surface +- SHOULD prefer [`Base UI`](https://base-ui.com/react/components) for new primitives if compatible with the stack +- MUST add an `aria-label` to icon-only buttons +- NEVER rebuild keyboard or focus behavior by hand unless explicitly requested + +## Interaction + +- MUST use an `AlertDialog` for destructive or irreversible actions +- SHOULD use structural skeletons for loading states +- NEVER use `h-screen`, use `h-dvh` +- MUST respect `safe-area-inset` for fixed elements +- MUST show errors next to where the action happens +- NEVER block paste in `input` or `textarea` elements + +## Animation + +- NEVER add animation unless it is explicitly requested +- MUST animate only compositor props (`transform`, `opacity`) +- NEVER animate layout properties (`width`, `height`, `top`, `left`, `margin`, `padding`) +- SHOULD avoid animating paint properties (`background`, `color`) except for small, local UI (text, icons) +- SHOULD use `ease-out` on entrance +- NEVER exceed `200ms` for interaction feedback +- MUST pause looping animations when off-screen +- SHOULD respect `prefers-reduced-motion` +- NEVER introduce custom easing curves unless explicitly requested +- SHOULD avoid animating large images or full-screen surfaces + +## Typography + +- MUST use `text-balance` for headings and `text-pretty` for body/paragraphs +- MUST use `tabular-nums` for data +- SHOULD use `truncate` or `line-clamp` for dense UI +- NEVER modify `letter-spacing` (`tracking-*`) unless explicitly requested + +## Layout + +- MUST use a fixed `z-index` scale (no arbitrary `z-*`) +- SHOULD use `size-*` for square elements instead of `w-*` + `h-*` + +## Performance + +- NEVER animate large `blur()` or `backdrop-filter` surfaces +- NEVER apply `will-change` outside an active animation +- NEVER use `useEffect` for anything that can be expressed as render logic + +## Design + +- NEVER use gradients unless explicitly requested +- NEVER use purple or multicolor gradients +- NEVER use glow effects as primary affordances +- SHOULD use Tailwind CSS default shadow scale unless explicitly requested +- MUST give empty states one clear next action +- SHOULD limit accent color usage to one per view +- SHOULD use existing theme or Tailwind CSS color tokens before introducing new ones diff --git a/.claude/skills/checkout-branch/skill.md b/.claude/skills/checkout-branch/skill.md index 6a1505f..a73e121 100644 --- a/.claude/skills/checkout-branch/skill.md +++ b/.claude/skills/checkout-branch/skill.md @@ -6,70 +6,86 @@ mode: bypassPermissions # 作業ブランチ チェックアウト スキル -指定したissue番号をもとに、対象サブモジュールに作業ブランチを作成する。 -**確認なしで即実行する。** +指定issue番号でサブモジュールに作業ブランチを作成。**確認なしで即実行**。 -## 対象リポジトリ +## 対象リポジトリとベースブランチ -サブモジュールのみを対象とする: -- `front/` → `ippei-shimizu/buzzbase_front`(ベースブランチ: `stg`) -- `back/` → `ippei-shimizu/buzzbase_back`(ベースブランチ: `stg`) -- `mobile/` → `ippei-shimizu/buzzbase_mobile`(ベースブランチ: `main`) +| パス | リポジトリ | ベース | +| ---- | ---------- | ------ | +| `/Users/shimizuippei/projects/dev/buzzbase/front` | `ippei-shimizu/buzzbase_front` | `stg` | +| `/Users/shimizuippei/projects/dev/buzzbase/back` | `ippei-shimizu/buzzbase_back` | `stg` | +| `/Users/shimizuippei/projects/dev/buzzbase/mobile` | `ippei-shimizu/buzzbase_mobile` | `main` | + +ルートリポジトリ(buzzbase)ではブランチ作成しない。 + +## トークン削減原則 + +- issue取得は **`title,labels` のみ** で済む場合は body を取らない(タイトルとラベルでプレフィックス・対象が判定できるなら body 不要) +- body が必要な場合のみ追加で `gh issue view ... --json body` を実行 +- 既存の作業状況確認(`status --short`)は **対象サブモジュールに対してのみ**実行 +- ブランチ作成は **fetch + checkout -b の2コマンド**(checkout→pull→checkout -b の3コマンドから短縮) ## ワークフロー -### 1. 引数の解析 +### 1. 引数解析 -`$ARGUMENTS` からissue番号を取得する。引数が空の場合はユーザーにissue番号を質問する。 +`$ARGUMENTS` からissue番号取得。空なら質問。 -### 2. issue情報の取得 +### 2. issue情報取得(最小フィールド) ```bash -gh issue view --repo ippei-shimizu/buzzbase --json title,body,labels +gh issue view --repo ippei-shimizu/buzzbase --json title,labels ``` -### 3. ブランチ名の決定 +タイトル・ラベルから対象とプレフィックスが特定できれば body は不要。曖昧な場合のみ追加で body を取得: -issueのラベルやタイトルからプレフィックスを判断する: -- `bug` ラベル / `Bug:` → `fix/` -- `enhancement` ラベル / `Feature:` `Perf:` `Improve:` → `feature/` -- `Refactor:` → `refactor/` -- `Chore:` → `chore/` -- その他 → `feature/` +```bash +gh issue view --repo ippei-shimizu/buzzbase --json body +``` -ブランチ名の形式: `/-<短い英語の説明>` +### 3. ブランチ名決定 -例: -- `feature/116-game-result-recording` -- `fix/138-ios-build-failure` +プレフィックス判定: -**注意**: ブランチ名に `#` を使用しない。 +| 条件 | プレフィックス | +| ---- | -------------- | +| `bug` ラベル / タイトル `Bug:` | `fix/` | +| `Refactor:` | `refactor/` | +| `Chore:` | `chore/` | +| `enhancement` ラベル / `Feature:` `Perf:` `Improve:` / その他 | `feature/` | -### 4. 影響範囲の確認 +形式: `/-<短い英語の説明>`(例: `feature/116-game-result-recording`、`fix/138-ios-build-failure`) -issueのタイトル・本文から対象サブモジュールを判断する: -- `[Mobile]` → `mobile/` -- フロントエンド関連 → `front/` -- バックエンド関連 → `back/` -- 両方に影響 → `front/` と `back/` -- 不明な場合はユーザーに確認する +**ブランチ名に `#` を使わない**(CI/CD互換性)。 -### 5. ブランチの作成 +### 4. 対象サブモジュール特定 -**確認なしで即実行する。** 対象サブモジュールごとに以下を実行する: +タイトル・ラベルから判定: + +- `[Mobile]` / モバイル関連 → `mobile/` +- フロント関連 → `front/` +- バックエンド関連 → `back/` +- 両方 → `front/` と `back/` +- 不明 → ユーザーに確認 + +### 5. ブランチ作成(対象サブモジュールのみ、各2コマンド) ```bash -cd /Users/shimizuippei/projects/dev/buzzbase/ +# 1. ベースブランチを最新化(fetchはローカルブランチを切り替えない) +git -C fetch origin + +# 2. fetched origin/ から新ブランチを作成 +git -C checkout -b origin/ +``` -# ベースブランチを最新に更新 -git checkout -git pull origin +実行前確認: -# 作業ブランチを作成してチェックアウト -git checkout -b +```bash +# 未コミット変更があるか +git -C status --short ``` -複数のサブモジュールが対象の場合は、それぞれで実行する。 +未コミット変更がある場合はユーザーに通知し、stashするか確認。同名ブランチが既に存在する場合もユーザーに確認。 ### 6. 完了報告 @@ -77,18 +93,16 @@ git checkout -b ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ブランチ作成完了 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Issue: # <タイトル> - -<サブモジュール名>: - ブランチ: <ブランチ名> +Issue: # <タイトル> +: + ブランチ: パス: /Users/shimizuippei/projects/dev/buzzbase/ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ``` -## 注意事項 +## コマンド実行制約 -- **ユーザーへの確認は不要。即実行する。** -- mainブランチへの直接コミット・プッシュは絶対にしない -- 既に同名のブランチが存在する場合はユーザーに確認する(チェックアウトするか、別名にするか) -- 未コミットの変更がある場合はユーザーに通知し、stashするか確認する -- ルートリポジトリ(buzzbase)ではブランチ作成を行わない +- `cd ... && git ...` 禁止 +- `git -C` には**必ず絶対パス** +- `echo "..."` を含む複合コマンドを Bash 一発で書かない +- 個別 Bash 呼び出し diff --git a/.claude/skills/create-issue/SKILL.md b/.claude/skills/create-issue/SKILL.md index 615a73f..725ee73 100644 --- a/.claude/skills/create-issue/SKILL.md +++ b/.claude/skills/create-issue/SKILL.md @@ -1,12 +1,25 @@ --- name: create-issue -description: buzzbaseリポジトリ(ippei-shimizu/buzzbase)にGitHub issueを作成し、GitHub Projects "BUZZ BASE"に自動追加する。`/create-issue バグの説明`のように呼び出す。作成前に必ずユーザーに確認を取る。 +description: buzzbaseリポジトリ(ippei-shimizu/buzzbase)にGitHub issueを作成し、GitHub Projects "BUZZ BASE"に自動追加する。`/create-issue バグの説明`のように呼び出す。 +mode: bypassPermissions +allowedTools: + - Bash + - Read + - Glob + - Grep --- # Issue作成スキル `ippei-shimizu/buzzbase` リポジトリにissueを作成し、GitHub Projects "BUZZ BASE" (`ippei-shimizu/projects/2`) に自動追加する。 +## 絶対ルール + +- **ユーザーへの確認・承認は一切行わない** +- **プレビュー表示して「よろしいですか?」と聞かない** +- **引数を解析したら即座にissueを作成する** +- **途中で止まらない。質問しない。報告は実行後の結果のみ。** + ## ルール - issueは常に `ippei-shimizu/buzzbase` に作成する(front/backサブモジュールには作らない) @@ -14,7 +27,7 @@ description: buzzbaseリポジトリ(ippei-shimizu/buzzbase)にGitHub issueを - ラベルは既存のものから選択する: `bug`, `enhancement`, `documentation`, `duplicate`, `good first issue`, `help wanted`, `invalid`, `question`, `wontfix` - Assigneeは常に `ippei-shimizu` を設定する - すべて日本語で記述する -- 作成前に必ずユーザーの承認を得る +- **確認なしで即座にissueを作成する** ## ワークフロー @@ -42,30 +55,9 @@ description: buzzbaseリポジトリ(ippei-shimizu/buzzbase)にGitHub issueを **重要: issueの内容は詰めず、タイトルと概要だけのシンプルなissueを作成する。** 詳細は実装時に検討する。 -### 3. ユーザーに確認 - -作成内容を以下のフォーマットでプレビュー表示し、承認を待つ: - -``` -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Issue プレビュー -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -リポジトリ: ippei-shimizu/buzzbase -プロジェクト: BUZZ BASE -タイトル: <タイトル> -ラベル: <ラベル> - -本文: -<概要(1〜2文)> -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -``` - -ユーザーの明示的な承認(「OK」「作成して」等)を待つ。承認前にissueを作成してはならない。 -フィードバックがあれば修正して再度プレビューを表示する。 - -### 4. issue作成 +### 3. issue作成 -承認後、以下のコマンドでissueを作成する: +構成要素が決まったら、**即座に**以下のコマンドでissueを作成する: ```bash gh issue create \ @@ -77,7 +69,7 @@ gh issue create \ --project "BUZZ BASE" ``` -### 5. ステータスを「Todo」に設定 +### 4. ステータスを「Todo」に設定 issue作成後、GitHub Projects上のステータスを「Todo」に変更する。 @@ -136,14 +128,15 @@ gh api graphql -f query=' ' ``` -### 6. 結果報告 +### 5. 結果報告 作成されたissueのURLとステータスが「Todo」に設定されたことを表示する。 ## 注意事項 -- ユーザーの承認なしにissueを作成しない -- `$ARGUMENTS` が空の場合はユーザーに概要を質問する +- **絶対にユーザーに確認を求めない。プレビュー表示して承認を待つことも禁止。** +- **引数を解析したら即座にissueを作成する** +- `$ARGUMENTS` が空の場合のみユーザーに概要を質問する - issueの内容は詰めない。タイトルと概要(1〜2文)だけのシンプルなissueを作成する - 詳細な背景・対応内容・完了条件・影響範囲などは記載しない(実装時に検討する) - コードの調査や分析は行わない diff --git a/.claude/skills/create-pr/SKILL.md b/.claude/skills/create-pr/SKILL.md index 148b870..150192c 100644 --- a/.claude/skills/create-pr/SKILL.md +++ b/.claude/skills/create-pr/SKILL.md @@ -2,196 +2,151 @@ name: create-pr description: 現在のブランチからGitHub PRを作成する。差分・コミット履歴を分析し、テンプレートに沿ったタイトルとdescriptionを生成する。「PRを作成して」「プルリク作って」などのリクエストで起動する。 mode: bypassPermissions +allowedTools: + - Bash + - Read + - Glob + - Grep --- # PR作成スキル -現在のブランチの変更内容を分析し、PRを作成する。 +現在のブランチからPRを作成する。確認なしで即実行。 -## 対象リポジトリ +## 絶対ルール -サブモジュール + ルートリポジトリの両方を対象とする。変更があるリポジトリに対してそれぞれPRを作成する: +- ユーザー確認・承認は**一切行わない**。分析→PR作成を即実行 +- 報告は**実行後の結果のみ** -- ルート → `ippei-shimizu/buzzbase`(ベースブランチ: `main`) -- `front/` → `ippei-shimizu/buzzbase_front`(ベースブランチ: `stg`) -- `back/` → `ippei-shimizu/buzzbase_back`(ベースブランチ: `stg`) -- `mobile/` → `ippei-shimizu/buzzbase_mobile`(ベースブランチ: `main`) +## 対象リポジトリとベースブランチ -複数のリポジトリに変更がある場合はそれぞれPRを作成する。issueはメインリポジトリ(`ippei-shimizu/buzzbase`)で管理されているため、issueの検索・close指定はメインリポジトリを参照する。 +| パス | リポジトリ | ベースブランチ | +| ---- | ---------- | -------------- | +| `/Users/shimizuippei/projects/dev/buzzbase` | `ippei-shimizu/buzzbase` | `main` | +| `/Users/shimizuippei/projects/dev/buzzbase/front` | `ippei-shimizu/buzzbase_front` | `stg` | +| `/Users/shimizuippei/projects/dev/buzzbase/back` | `ippei-shimizu/buzzbase_back` | `stg` | +| `/Users/shimizuippei/projects/dev/buzzbase/mobile` | `ippei-shimizu/buzzbase_mobile` | `main` | -## ルール +issueはメインリポジトリ(`ippei-shimizu/buzzbase`)で集約管理。サブモジュールのPRでもメインリポジトリのissueを参照する。 -- PRは変更のあるサブモジュールのリポジトリに作成する(上記参照) -- ベースブランチはデフォルトで `stg`(`$ARGUMENTS` で指定があればそちらを使用) -- **mobileサブモジュールのみベースブランチは `main`**(front/backは `stg`) -- Assigneeは常に `ippei-shimizu` を設定する -- すべて日本語で記述する -- **確認なしで即座にPRを作成する**(プレビュー・承認ステップは不要) -- リモートにプッシュされていない場合は先にプッシュする +## トークン削減原則 -## ワークフロー +- **`status --short` を最初に1回**回し、変更ありリポジトリだけに絞る。他はスキップ +- description は**コミットメッセージ(`log --format="%s"`)+ ファイル統計(`diff --stat`)から生成**。フルdiffは投げない +- diff の中身が必要な場合のみ `diff --name-status` を追加で取得 +- 会話文脈に直前の作業があれば**それを最優先でdescriptionに反映**(git解析を最小化) -### 1. 現在のブランチ状態を確認 +## ワークフロー -各リポジトリの状態を確認する: +### 1. 変更ありリポジトリの絞り込み ```bash -# ルートリポジトリ -git branch --show-current -git status --short -git log origin/$(git branch --show-current)..HEAD --oneline 2>/dev/null -git log origin/main..HEAD --oneline -git diff origin/main...HEAD --stat - -# backサブモジュール -cd back -git branch --show-current -git status --short -git log origin/$(git branch --show-current)..HEAD --oneline 2>/dev/null -git log stg..HEAD --oneline -git diff stg...HEAD --stat - -# frontサブモジュール -cd front -git branch --show-current -git status --short -git log origin/$(git branch --show-current)..HEAD --oneline 2>/dev/null -git log stg..HEAD --oneline -git diff stg...HEAD --stat - -# mobileサブモジュール(ベースブランチは main) -cd mobile -git branch --show-current -git status --short -git log origin/$(git branch --show-current)..HEAD --oneline 2>/dev/null -git log main..HEAD --oneline -git diff main...HEAD --stat +git -C /Users/shimizuippei/projects/dev/buzzbase status --short +git -C /Users/shimizuippei/projects/dev/buzzbase/front status --short +git -C /Users/shimizuippei/projects/dev/buzzbase/back status --short +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile status --short ``` -変更のないリポジトリはスキップする。 -未コミットの変更がある場合は先に `/smart-commit` でコミットする。 +未コミット変更がある場合は先に `/smart-commit` で処理。 -### 2. 差分の分析 +各リポジトリで `branch --show-current` してブランチ名取得。ベースブランチと一致するリポジトリ・空のリポジトリはスキップ。 -変更のある各サブモジュールについて分析する: +### 2. 変更内容の取得(変更ありリポジトリのみ) ```bash -cd - -# コミットメッセージ一覧 -git log stg..HEAD --format="%s" +# コミットメッセージ +git -C log ..HEAD --format="%s" -# 変更ファイル一覧 -git diff stg...HEAD --stat - -# 詳細な差分(大きい場合はサブエージェントで分析) -git diff stg...HEAD +# ファイル統計 +git -C diff ...HEAD --stat ``` -以下を把握する: -- 変更されたファイルとその種類(新規/変更/削除) -- 実装の目的と内容 -- 影響範囲(front / back / 両方) +これだけで title / description は十分書ける。**フルdiffは原則投げない**。 + +`` は表参照(front/back: `stg`、mobile/ルート: `main`)。`$ARGUMENTS` でベース指定があれば優先。 -### 3. 関連issueの特定 +### 3. issue番号特定 -以下の手順で関連するissue番号を特定する: +優先順位: -1. **ブランチ名から抽出**: ブランチ名に含まれるissue番号を確認(例: `feature/123-some-feature` → #123、`fix/issue-45` → #45) -2. **コミットメッセージから抽出**: `#123`、`close #123`、`fix #123` などのパターンを検索 -3. **GitHub Projects から検索**: 上記で見つからない場合、PRの内容に関連するissueをGitHub Projectsから検索する +1. ブランチ名から抽出(例: `chore/154-...` → #154、`fix/issue-45` → #45) +2. コミットメッセージから抽出(`#123`, `close #123`, `fix #123`) +3. 上記で不明なら、メインリポジトリの open issues から PR内容に近いものを検索: ```bash -# メインリポジトリのopen issueを一覧取得し、PRの内容に関連するものを探す -gh issue list --repo ippei-shimizu/buzzbase --state open --limit 30 --json number,title,body +gh issue list --repo ippei-shimizu/buzzbase --state open --limit 30 --json number,title ``` -見つかった場合は `close #ISSUE_NUMBER` に設定する。 -**注意**: issueはメインリポジトリ(ippei-shimizu/buzzbase)に集約されている場合があるため、サブモジュールのPRでもメインリポジトリのissueを検索すること。 +見つかれば `close #ISSUE_NUMBER`。**推測で番号を入れない**。不明なら空欄。 -### 4. PRタイトルの生成 +### 4. タイトル・description生成 -- コミット履歴と差分から変更の主目的を要約する -- 70文字以内の簡潔な日本語タイトル -- `$ARGUMENTS` にタイトルのヒントがあればそれを優先する - -### 5. PR descriptionの生成 - -`/pr-description` スキルと同じテンプレートに沿って生成する: +- タイトル: 70文字以内・日本語・コミットメッセージから要約。`$ARGUMENTS` にヒントあれば優先 +- description: 下のテンプレートに沿う。**会話文脈優先**、不足部分のみコミット情報で補完 ```markdown ## issue close #ISSUE_NUMBER ## 実装概要 - + ## 背景 - + ## やらなかったこと - + ## 受入基準 - - [ ] 基準1 - [ ] 基準2 ## 実装詳細 - + ## スクリーンショット - + ## 確認手順 - 1. 手順1 2. 手順2 ## 影響範囲 - ## コード上の懸念点 - ## その他 - + ``` -### 6. プッシュとPR作成 - -変更のある各サブモジュールに対して即座に実行する: +### 5. push + PR作成(即実行) ```bash -cd - -# リモートにプッシュされていない場合 -git push -u origin $(git branch --show-current) +# 必要ならpush +git -C push -u origin -# PR作成(リポジトリはサブモジュールに対応するものを使用) -# ベースブランチ: front/back は stg、mobile は main +# PR作成 gh pr create \ - --repo ippei-shimizu/buzzbase_ \ - --base \ - --title "<タイトル>" \ + --repo \ + --head \ + --base \ + --title "" \ --assignee ippei-shimizu \ --body "$(cat <<'EOF' -<description内容> +<description> EOF )" ``` -両方のサブモジュールに変更がある場合は2つのPRを作成する。 +複数リポジトリに変更がある場合は各々で作成。 -### 7. 結果報告 +### 6. 結果報告 -作成されたPRのURLを表示する。複数の場合はすべてのURLを一覧表示する。 +作成された PR の URL を一覧表示。 -## 注意事項 +## コマンド実行制約 -- **確認なしで即座にPR作成・pushを実行する** -- 差分から読み取れる事実に基づいて記述し、推測が入る箇所は明示する -- issue番号が特定できない場合は `close #` の行は空欄にする(推測で番号を入れない) -- 関連issueが見つかった場合は必ず `close #ISSUE_NUMBER` を設定する -- `$ARGUMENTS` が指定された場合、それをPRの背景コンテキストとして活用する -- 会話の文脈(直前の実装作業など)がある場合は、それを活用してdescriptionを充実させる +- `cd ... && git ...` 禁止 +- `git -C` には**必ず絶対パス** +- `echo "..."` を含む複合コマンドを Bash 一発で書かない +- コマンドは個別 Bash 呼び出し diff --git a/.claude/skills/fixing-accessibility/SKILL.md b/.claude/skills/fixing-accessibility/SKILL.md new file mode 100644 index 0000000..e2dcd70 --- /dev/null +++ b/.claude/skills/fixing-accessibility/SKILL.md @@ -0,0 +1,136 @@ +--- +name: fixing-accessibility +description: Audit and fix HTML accessibility issues including ARIA labels, keyboard navigation, focus management, color contrast, and form errors. Use when adding interactive controls, forms, dialogs, or reviewing WCAG compliance. +--- + +# fixing-accessibility + +Fix accessibility issues. + +## how to use + +- `/fixing-accessibility` + Apply these constraints to any UI work in this conversation. + +- `/fixing-accessibility <file>` + Review the file against all rules below and report: + - violations (quote the exact line or snippet) + - why it matters (one short sentence) + - a concrete fix (code-level suggestion) + +Do not rewrite large parts of the UI. Prefer minimal, targeted fixes. + +## when to apply + +Reference these guidelines when: +- adding or changing buttons, links, inputs, menus, dialogs, tabs, dropdowns +- building forms, validation, error states, helper text +- implementing keyboard shortcuts or custom interactions +- working on focus states, focus trapping, or modal behavior +- rendering icon-only controls +- adding hover-only interactions or hidden content + +## rule categories by priority + +| priority | category | impact | +|----------|----------|--------| +| 1 | accessible names | critical | +| 2 | keyboard access | critical | +| 3 | focus and dialogs | critical | +| 4 | semantics | high | +| 5 | forms and errors | high | +| 6 | announcements | medium-high | +| 7 | contrast and states | medium | +| 8 | media and motion | low-medium | +| 9 | tool boundaries | critical | + +## quick reference + +### 1. accessible names (critical) + +- every interactive control must have an accessible name +- icon-only buttons must have aria-label or aria-labelledby +- every input, select, and textarea must be labeled +- links must have meaningful text (no “click here”) +- decorative icons must be aria-hidden + +### 2. keyboard access (critical) + +- do not use div or span as buttons without full keyboard support +- all interactive elements must be reachable by Tab +- focus must be visible for keyboard users +- do not use tabindex greater than 0 +- Escape must close dialogs or overlays when applicable + +### 3. focus and dialogs (critical) + +- modals must trap focus while open +- restore focus to the trigger on close +- set initial focus inside dialogs +- opening a dialog should not scroll the page unexpectedly + +### 4. semantics (high) + +- prefer native elements (button, a, input) over role-based hacks +- if a role is used, required aria attributes must be present +- lists must use ul or ol with li +- do not skip heading levels +- tables must use th for headers when applicable + +### 5. forms and errors (high) + +- errors must be linked to fields using aria-describedby +- required fields must be announced +- invalid fields must set aria-invalid +- helper text must be associated with inputs +- disabled submit actions must explain why + +### 6. announcements (medium-high) + +- critical form errors should use aria-live +- loading states should use aria-busy or status text +- toasts must not be the only way to convey critical information +- expandable controls must use aria-expanded and aria-controls + +### 7. contrast and states (medium) + +- ensure sufficient contrast for text and icons +- hover-only interactions must have keyboard equivalents +- disabled states must not rely on color alone +- do not remove focus outlines without a visible replacement + +### 8. media and motion (low-medium) + +- images must have correct alt text (meaningful or empty) +- videos with speech should provide captions when relevant +- respect prefers-reduced-motion for non-essential motion +- avoid autoplaying media with sound + +### 9. tool boundaries (critical) + +- prefer minimal changes, do not refactor unrelated code +- do not add aria when native semantics already solve the problem +- do not migrate UI libraries unless requested + +## common fixes + +```html +<!-- icon-only button: add aria-label --> +<!-- before --> <button><svg>...</svg></button> +<!-- after --> <button aria-label="Close"><svg aria-hidden="true">...</svg></button> + +<!-- div as button: use native element --> +<!-- before --> <div onclick="save()">Save</div> +<!-- after --> <button onclick="save()">Save</button> + +<!-- form error: link with aria-describedby --> +<!-- before --> <input id="email" /> <span>Invalid email</span> +<!-- after --> <input id="email" aria-describedby="email-err" aria-invalid="true" /> <span id="email-err">Invalid email</span> +``` + +## review guidance + +- fix critical issues first (names, keyboard, focus, tool boundaries) +- prefer native HTML before adding aria +- quote the exact snippet, state the failure, propose a small fix +- for complex widgets (menu, dialog, combobox), prefer established accessible primitives over custom behavior \ No newline at end of file diff --git a/.claude/skills/fixing-metadata/SKILL.md b/.claude/skills/fixing-metadata/SKILL.md new file mode 100644 index 0000000..3c30092 --- /dev/null +++ b/.claude/skills/fixing-metadata/SKILL.md @@ -0,0 +1,112 @@ +--- +name: fixing-metadata +description: > + Audit and fix HTML metadata including page titles, meta descriptions, canonical URLs, Open Graph + tags, Twitter cards, favicons, JSON-LD structured data, and robots directives. Use when adding + SEO metadata, fixing social share previews, reviewing Open Graph tags, setting up canonical URLs, + or shipping new pages that need correct meta tags. +version: 1.0.1 +license: MIT +--- + +## Workflow + +1. Identify pages with missing or incorrect metadata (titles, descriptions, canonical, OG tags) +2. Audit against the priority rules below — fix critical issues (duplicates, indexing) first +3. Ensure title, description, canonical, and og:url all agree with each other +4. Verify social cards render correctly on a real URL, not localhost +5. Keep diffs minimal and scoped to metadata only — do not refactor unrelated code +## when to apply + +Reference these guidelines when: +- adding or changing page titles, descriptions, canonical, robots +- implementing Open Graph or Twitter card metadata +- setting favicons, app icons, manifest, theme-color +- building shared SEO components or layout metadata defaults +- adding structured data (JSON-LD) +- changing locale, alternate languages, or canonical routing +- shipping new pages, marketing pages, or shareable links + +## rule categories by priority + +| priority | category | impact | +|----------|----------|--------| +| 1 | correctness and duplication | critical | +| 2 | title and description | high | +| 3 | canonical and indexing | high | +| 4 | social cards | high | +| 5 | icons and manifest | medium | +| 6 | structured data | medium | +| 7 | locale and alternates | low-medium | +| 8 | tool boundaries | critical | + +## quick reference + +### 1. correctness and duplication (critical) + +- define metadata in one place per page, avoid competing systems +- do not emit duplicate title, description, canonical, or robots tags +- metadata must be deterministic, no random or unstable values +- escape and sanitize any user-generated or dynamic strings +- every page must have safe defaults for title and description + +### 2. title and description (high) + +- every page must have a title +- use a consistent title format across the site +- keep titles short and readable, avoid stuffing +- shareable or searchable pages should have a meta description +- descriptions must be plain text, no markdown or quote spam + +### 3. canonical and indexing (high) + +- canonical must point to the preferred URL for the page +- use noindex only for private, duplicate, or non-public pages +- robots meta must match actual access intent +- previews or staging pages should be noindex by default when possible +- paginated pages must have correct canonical behavior + +### 4. social cards (high) + +- shareable pages must set Open Graph title, description, and image +- Open Graph and Twitter images must use absolute URLs +- prefer correct image dimensions and stable aspect ratios +- og:url must match the canonical URL +- use a sensible og:type, usually website or article +- set twitter:card appropriately, summary_large_image by default + +### 5. icons and manifest (medium) + +- include at least one favicon that works across browsers +- include apple-touch-icon when relevant +- manifest must be valid and referenced when used +- set theme-color intentionally to avoid mismatched UI chrome +- icon paths should be stable and cacheable + +### 6. structured data (medium) + +- do not add JSON-LD unless it clearly maps to real page content +- JSON-LD must be valid and reflect what is actually rendered +- do not invent ratings, reviews, prices, or organization details +- prefer one structured data block per page unless required + +### 7. locale and alternates (low-medium) + +- set the html lang attribute correctly +- set og:locale when localization exists +- add hreflang alternates only when pages truly exist +- localized pages must canonicalize correctly per locale + +### 8. tool boundaries (critical) + +- prefer minimal changes, do not refactor unrelated code +- do not migrate frameworks or SEO libraries unless requested +- follow the project's existing metadata pattern (Next.js metadata API, react-helmet, manual head, etc.) + +## review guidance + +- fix critical issues first (duplicates, canonical, indexing) +- ensure title, description, canonical, and og:url agree +- verify social cards on a real URL, not localhost +- prefer stable, boring metadata over clever or dynamic +- keep diffs minimal and scoped to metadata only diff --git a/.claude/skills/fixing-motion-performance/SKILL.md b/.claude/skills/fixing-motion-performance/SKILL.md new file mode 100644 index 0000000..7d22f2e --- /dev/null +++ b/.claude/skills/fixing-motion-performance/SKILL.md @@ -0,0 +1,151 @@ +--- +name: fixing-motion-performance +description: Audit and fix animation performance issues including layout thrashing, compositor properties, scroll-linked motion, and blur effects. Use when animations stutter, transitions jank, or reviewing CSS/JS animation performance. +--- + +# fixing-motion-performance + +Fix animation performance issues. + +## how to use + +- `/fixing-motion-performance` + Apply these constraints to any UI animation work in this conversation. + +- `/fixing-motion-performance <file>` + Review the file against all rules below and report: + - violations (quote the exact line or snippet) + - why it matters (one short sentence) + - a concrete fix (code-level suggestion) + +Do not migrate animation libraries unless explicitly requested. Apply rules within the existing stack. + +## when to apply + +Reference these guidelines when: +- adding or changing UI animations (CSS, WAAPI, Motion, rAF, GSAP) +- refactoring janky interactions or transitions +- implementing scroll-linked motion or reveal-on-scroll +- animating layout, filters, masks, gradients, or CSS variables +- reviewing components that use will-change, transforms, or measurement + +## rendering steps glossary + +- composite: transform, opacity +- paint: color, borders, gradients, masks, images, filters +- layout: size, position, flow, grid, flex + +## rule categories by priority + +| priority | category | impact | +|----------|----------|--------| +| 1 | never patterns | critical | +| 2 | choose the mechanism | critical | +| 3 | measurement | high | +| 4 | scroll | high | +| 5 | paint | medium-high | +| 6 | layers | medium | +| 7 | blur and filters | medium | +| 8 | view transitions | low | +| 9 | tool boundaries | critical | + +## quick reference + +### 1. never patterns (critical) + +- do not interleave layout reads and writes in the same frame +- do not animate layout continuously on large or meaningful surfaces +- do not drive animation from scrollTop, scrollY, or scroll events +- no requestAnimationFrame loops without a stop condition +- do not mix multiple animation systems that each measure or mutate layout + +### 2. choose the mechanism (critical) + +- default to transform and opacity for motion +- use JS-driven animation only when interaction requires it +- paint or layout animation is acceptable only on small, isolated surfaces +- one-shot effects are acceptable more often than continuous motion +- prefer downgrading technique over removing motion entirely + +### 3. measurement (high) + +- measure once, then animate via transform or opacity +- batch all DOM reads before writes +- do not read layout repeatedly during an animation +- prefer FLIP-style transitions for layout-like effects +- prefer approaches that batch measurement and writes + +### 4. scroll (high) + +- prefer Scroll or View Timelines for scroll-linked motion when available +- use IntersectionObserver for visibility and pausing +- do not poll scroll position for animation +- pause or stop animations when off-screen +- scroll-linked motion must not trigger continuous layout or paint on large surfaces + +### 5. paint (medium-high) + +- paint-triggering animation is allowed only on small, isolated elements +- do not animate paint-heavy properties on large containers +- do not animate CSS variables for transform, opacity, or position +- do not animate inherited CSS variables +- scope animated CSS variables locally and avoid inheritance + +### 6. layers (medium) + +- compositor motion requires layer promotion, never assume it +- use will-change temporarily and surgically +- avoid many or large promoted layers +- validate layer behavior with tooling when performance matters + +### 7. blur and filters (medium) + +- keep blur animation small (<=8px) +- use blur only for short, one-time effects +- never animate blur continuously +- never animate blur on large surfaces +- prefer opacity and translate before blur + +### 8. view transitions (low) + +- use view transitions only for navigation-level changes +- avoid view transitions for interaction-heavy UI +- avoid view transitions when interruption or cancellation is required +- treat size changes as potentially layout-triggering + +### 9. tool boundaries (critical) + +- do not migrate or rewrite animation libraries unless explicitly requested +- apply these rules within the existing animation system +- never partially migrate APIs or mix styles within the same component + +## common fixes + +```css +/* layout thrashing: animate transform instead of width */ +/* before */ .panel { transition: width 0.3s; } +/* after */ .panel { transition: transform 0.3s; } + +/* scroll-linked: use scroll-timeline instead of JS */ +/* before */ window.addEventListener('scroll', () => el.style.opacity = scrollY / 500) +/* after */ .reveal { animation: fade-in linear; animation-timeline: view(); } +``` + +```js +// measurement: batch reads before writes (FLIP) +// before — layout thrash +el.style.left = el.getBoundingClientRect().left + 10 + 'px'; +// after — measure once, animate via transform +const first = el.getBoundingClientRect(); +el.classList.add('moved'); +const last = el.getBoundingClientRect(); +el.style.transform = `translateX(${first.left - last.left}px)`; +requestAnimationFrame(() => { el.style.transition = 'transform 0.3s'; el.style.transform = ''; }); +``` + +## review guidance + +- enforce critical rules first (never patterns, tool boundaries) +- choose the least expensive rendering work that matches the intent +- for any non-default choice, state the constraint that justifies it (surface size, duration, or interaction requirement) +- when reviewing, prefer actionable notes and concrete alternatives over theory diff --git a/.claude/skills/smart-commit/SKILL.md b/.claude/skills/smart-commit/SKILL.md index c61a415..46dc5a9 100644 --- a/.claude/skills/smart-commit/SKILL.md +++ b/.claude/skills/smart-commit/SKILL.md @@ -2,108 +2,114 @@ name: smart-commit description: 現在の作業差分を分析し、適切な粒度でコミットを分割し、自動で add / commit / push まで実行する。「コミットして」「コミットを分けて」「差分を整理して」などのリクエストで起動する。 mode: bypassPermissions +allowedTools: + - Bash + - Read + - Glob + - Grep --- # Smart Commit スキル -作業差分を分析し、論理的な単位でコミットを分割して、自動で git add / commit / push まで実行する。 -**承認なしで即実行する。途中で確認を挟まない。** +作業差分を論理的単位で分割し、自動で add / commit / push する。 -## 対象リポジトリ +## 絶対ルール -サブモジュール + ルートリポジトリの両方を対象とする。 +- ユーザー確認・承認は**一切行わない**。差分分析→add→commit→push を一気に実行 +- mainブランチでの直接コミット・pushは禁止(検出時は中断してエラー) +- 報告は**実行後のサマリーのみ** -- `front/` → `ippei-shimizu/buzzbase_front` -- `back/` → `ippei-shimizu/buzzbase_back` -- `mobile/` → `ippei-shimizu/buzzbase_mobile` -- ルート → `ippei-shimizu/buzzbase` +## 対象リポジトリ(絶対パス使用) -## ワークフロー +| リポジトリ | パス | +| ---------- | ---- | +| ルート | `/Users/shimizuippei/projects/dev/buzzbase` | +| front | `/Users/shimizuippei/projects/dev/buzzbase/front` | +| back | `/Users/shimizuippei/projects/dev/buzzbase/back` | +| mobile | `/Users/shimizuippei/projects/dev/buzzbase/mobile` | + +## トークン削減原則 -### 1. 差分の取得 +- 最初に `status --short` で**変更があるリポジトリだけに絞る**。他は以後すべてスキップ +- 分類は **`diff --stat` + `diff --name-status`** で十分。フルdiffは**分類が曖昧な場合のみ**個別ファイル単位で取得 +- 新規ファイルの中身は**ファイル名・拡張子から推測**で分類。中身読み込みは最終手段 -各リポジトリの差分を取得する: +## ワークフロー + +### 1. 変更検出(status --short のみを各リポジトリで1回) ```bash -# ルートリポジトリ(サブモジュール以外の変更) -git status --short -git diff HEAD +git -C /Users/shimizuippei/projects/dev/buzzbase status --short +git -C /Users/shimizuippei/projects/dev/buzzbase/front status --short +git -C /Users/shimizuippei/projects/dev/buzzbase/back status --short +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile status --short +``` -# 各サブモジュール -cd front && git status --short && git diff HEAD -cd back && git status --short && git diff HEAD -cd mobile && git status --short && git diff HEAD +出力が空のリポジトリは以後**完全にスキップ**(diff も log も投げない)。 -# 未追跡の新規ファイル内容 -git diff --no-index /dev/null <file> +### 2. ブランチ確認(変更ありリポジトリのみ) + +```bash +git -C <path> branch --show-current ``` -変更がないリポジトリはスキップする。 +`main` の場合は中断・エラー。 -### 2. 変更の分類 +### 3. グルーピング情報取得(変更ありリポジトリのみ) -各ファイルの変更内容を分析し、以下の観点でグルーピングする: +```bash +git -C <path> diff --stat HEAD +git -C <path> diff --name-status HEAD +``` -- **機能単位**: 同じ機能に関する変更はまとめる(例: コントローラ + テスト + ルーティング) -- **関心の分離**: 設定変更、リファクタリング、新機能、バグ修正は分ける -- **依存関係**: 後のコミットが前のコミットに依存する場合、依存される側を先にする +ファイル一覧 + 変更行数 + 種類(A/M/D)が取得できる。**フルdiffは投げない**。 -#### 分割の粒度ガイドライン +### 4. コミット分割 -- 1コミット = 1つの論理的変更(「なぜこの変更をしたか」を1文で説明できる単位) -- 設定ファイルの変更(Gemfile, package.json 等)は関連する機能変更と同じコミットに含める -- テストは対応する実装と同じコミットにする -- リンティング修正やフォーマット変更は独立したコミットにする -- ファイル削除は、その理由となる変更と同じコミットにまとめる +ファイルパス・拡張子・ステータスから分類: -### 3. コミットメッセージの生成 +- **機能単位**: 同じ機能・モジュール配下のファイルはまとめる +- **テスト**: 対応する実装と同コミット +- **設定ファイル**(package.json, Gemfile等): 関連する機能変更と同コミット +- **lint/format修正**: 単独コミット +- **削除のみ**: 理由となる変更と同コミット -プロジェクトのコミットプレフィックス規約に従う: -`Fix:`, `Add:`, `Update:`, `Change:`, `Refactor:`, `Remove:`, `Test:`, `Chore:`, `Docs:` +1コミット = 1つの「なぜ」で説明できる単位。 -メッセージは変更の「なぜ」を日本語で簡潔に記述する。 +### 5. コミットメッセージ生成 -### 4. 安全チェック +プレフィックス: `Add:` / `Fix:` / `Update:` / `Change:` / `Refactor:` / `Remove:` / `Test:` / `Chore:` / `Docs:` -実行前に以下を確認する(ユーザーに確認せず自動判定): +ファイル名・パスから内容推測。**曖昧な場合のみ**該当ファイルの diff を個別取得: -- 現在のブランチが `main` でないことを確認。`main` の場合は**中断してエラーを出す** -- サブモジュールも同様に `main` ブランチでないことを確認 +```bash +git -C <path> diff HEAD -- <file> +``` -### 5. 自動実行 +メッセージは日本語で「なぜ」を簡潔に。 -分類が完了したら、**承認を待たずに**以下を順番に自動実行する: +### 6. add / commit / push(自動実行) -1. **backサブモジュール**: `back/` ディレクトリで add → commit → push -2. **frontサブモジュール**: `front/` ディレクトリで add → commit → push -3. **mobileサブモジュール**: `mobile/` ディレクトリで add → commit → push -4. **ルートリポジトリ**: ルートで add → commit → push(サブモジュール参照更新含む) +実行順序: **back → front → mobile → ルート**(ルートはサブモジュール参照更新を含むため最後) -各リポジトリで `git push -u origin $(git branch --show-current)` を実行する。 -変更がないリポジトリはスキップする。 +```bash +git -C <path> add <files> +git -C <path> commit -m "Type: 説明" +git -C <path> push origin <branch> +``` -実行中は各コミットの内容を以下のフォーマットで出力する: +各コミット時に出力: ``` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -コミット 1/N [リポジトリ名] +コミット N/M [リポジトリ名] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -メッセージ: <prefix>: <コミットメッセージ> - -対象ファイル: - A path/to/new-file.ts - M path/to/modified-file.rb - D path/to/deleted-file.ts - -変更概要: <このコミットで何をしたかの1行説明> +メッセージ: Type: 説明 +ファイル: A app/foo.ts / M app/bar.ts ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ``` -ステータス記号: `A` = 新規追加, `M` = 変更, `D` = 削除 - -### 6. 完了報告 - -すべてのコミットとプッシュが完了したら、実行結果のサマリーを出力する: +### 7. 完了サマリー ``` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -116,10 +122,14 @@ root: N commits pushed ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ``` -## 注意事項 +## コマンド実行制約 + +- `cd ... && git ...` 禁止(権限プロンプトの原因) +- `git -C` には**必ず絶対パス** +- `echo "..."` を含む複合コマンドを Bash 一発で書かない +- コマンドは個別 Bash 呼び出し + +## 安全 -- **承認不要で自動実行する**(差分分析 → add → commit → push を一気に行う) -- **途中で確認を挟まない** -- mainブランチへの直接コミット・プッシュは絶対にしない(検出した場合はエラーを出して中断する) -- `$ARGUMENTS` が指定された場合、それを変更の背景コンテキストとして活用する -- `.env`, `credentials.json` 等の秘密情報を含むファイルはコミットしない +- `.env` / `credentials.*` / `master.key` 等の秘密情報はコミットしない +- `$ARGUMENTS` があれば変更の背景コンテキストとして活用 diff --git a/.claude/skills/sync-default/SKILL.md b/.claude/skills/sync-default/SKILL.md new file mode 100644 index 0000000..c61bc4a --- /dev/null +++ b/.claude/skills/sync-default/SKILL.md @@ -0,0 +1,96 @@ +--- +name: sync-default +description: サブモジュール(front, back, mobile)を各デフォルトブランチに切り替えてpullし、最新状態にする。`/sync-default`や「デフォルトブランチに戻して」「最新に同期して」などのリクエストで起動する。 +mode: bypassPermissions +--- + +# デフォルトブランチ同期スキル + +各サブモジュールを作業ブランチからデフォルトブランチに切り替え、最新化する。**確認なしで即実行**。 + +## 対象リポジトリとデフォルトブランチ + +| パス | デフォルト | +| ---- | ---------- | +| `/Users/shimizuippei/projects/dev/buzzbase/front` | `stg` | +| `/Users/shimizuippei/projects/dev/buzzbase/back` | `stg` | +| `/Users/shimizuippei/projects/dev/buzzbase/mobile` | `main` | + +ルートリポジトリ(buzzbase)は対象外。 + +## トークン削減原則 + +- 状態取得は **`status -sb` のみ**でブランチ名 + 未コミット変更を1コマンドで取得(旧: status + rev-parse の2コマンド) +- 既にデフォルトブランチなら `switch` をスキップ +- 各サブモジュールへの3コマンド(status / checkout / pull)は**並列実行**(独立しているため) +- pull は `--ff-only` で fast-forward 不可なら早期エラー(マージコミットを作らない) + +## ワークフロー + +### 1. 状態確認(3サブモジュール並列) + +```bash +git -C <path> status -sb +``` + +出力例: + +``` +## chore/154-mobile-ci-testing + M file.ts +``` + +1行目(`## <branch>`)からブランチ名を、2行目以降の有無で未コミット変更を判定。 + +未コミット変更がある場合: ユーザーに通知し、stash するか確認。変更なければそのまま進む。 + +### 2. デフォルトブランチへ切り替え + pull + +各サブモジュール(並列実行可): + +```bash +# 既にデフォルトブランチでない場合のみ +git -C <path> switch <default> + +# pull は常に実行(fast-forward 限定) +git -C <path> pull --ff-only origin <default> +``` + +`switch` は `checkout` の代替(ブランチ切り替え専用、より安全)。既にデフォルトブランチなら `switch` 不要、pull のみ。 + +### 3. 元の作業ブランチを削除(変更があったサブモジュールのみ) + +ステップ1でデフォルト以外のブランチにいた場合のみ: + +```bash +git -C <path> branch -d <旧ブランチ> +``` + +- `-d`(小文字)を使う。マージ済みブランチのみ削除、未マージなら警告で残す(強制削除しない) +- デフォルトブランチに既にいた場合はスキップ + +### 4. 完了報告 + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +デフォルトブランチ同期完了 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +front: stg ← <旧>(削除済み | 変更なし) +back: stg ← <旧>(削除済み | 変更なし) +mobile: main ← <旧>(削除済み | 変更なし) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +既にデフォルトブランチだった場合は `(変更なし)`、ブランチ削除に失敗した場合は `(未マージのため残存)` と表示。 + +## コマンド実行制約 + +- `cd ... && git ...` 禁止 +- `git -C` には**必ず絶対パス** +- `echo "..."` を含む複合コマンドを Bash 一発で書かない +- 個別 Bash 呼び出し(独立コマンドは並列) + +## 注意事項 + +- 未コミット変更がある場合のみユーザー確認(それ以外は即実行) +- pull 失敗時はエラー内容を報告し、他のサブモジュールは続行 diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 0000000..b5e8cfd --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,44 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + # Optional: Filter by PR author + # if: | + # github.event.pull_request.user.login == 'external-contributor' || + # github.event.pull_request.user.login == 'new-developer' || + # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' + plugins: 'code-review@claude-code-plugins' + prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000..d300267 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,50 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr:*)' + diff --git a/.gitignore b/.gitignore index 0e06e4e..2307800 100644 --- a/.gitignore +++ b/.gitignore @@ -1,41 +1,12 @@ -.serena - # Claude Code settings .claude/settings.local.json -.claude/skills/gstack/ -.claude/skills/autoplan -.claude/skills/benchmark -.claude/skills/browse -.claude/skills/canary -.claude/skills/careful -.claude/skills/codex -.claude/skills/connect-chrome -.claude/skills/cso -.claude/skills/design-consultation -.claude/skills/design-review -.claude/skills/document-release -.claude/skills/freeze -.claude/skills/gstack-upgrade -.claude/skills/guard -.claude/skills/investigate -.claude/skills/land-and-deploy -.claude/skills/office-hours -.claude/skills/plan-ceo-review -.claude/skills/plan-design-review -.claude/skills/plan-eng-review -.claude/skills/qa -.claude/skills/qa-only -.claude/skills/retro -.claude/skills/review -.claude/skills/setup-browser-cookies -.claude/skills/setup-deploy -.claude/skills/ship -.claude/skills/unfreeze # Dependencies package-lock.json cookies.txt .env +.mcp.json data/ +.superpowers/ diff --git a/.gitmodules b/.gitmodules index 2ec4a1f..a8fcac5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,12 +1,9 @@ [submodule "front"] path = front url = git@github.com:ippei-shimizu/buzzbase_front.git - ignore = all [submodule "back"] path = back url = git@github.com:ippei-shimizu/buzzbase_back.git - ignore = all [submodule "mobile"] path = mobile url = git@github.com:ippei-shimizu/buzzbase_mobile.git - ignore = all diff --git a/CLAUDE.md b/CLAUDE.md index ceb392b..4f053aa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,6 +34,13 @@ APIのベースパス: `/api/v1/` +## コードコメントの方針 + +- **デフォルトのコメント抑制ポリシー(複数行コメント禁止・1行のみ等)はこのプロジェクトでは適用しない** +- 公開メソッドの意図・引数・返り値を説明する複数行コメントや yardoc スタイル(`@param` / `@return`)は **積極的に書いてよい**。コードレビュー時に削除しないこと +- 「WHY が自明だから」を理由にレビューで指摘されたり削除されたりしない。意図・前提・返却フォーマットを明示するコメントは保守性向上の資産として残す +- 不要なのは「コードと同じことを言うだけのコメント」「TODO や履歴メモ」など。意味のあるドキュメンテーションコメントは残す + ## Gitルール - コミットメッセージは **日本語** で記述 diff --git a/back b/back index 2def0a4..96ff9da 160000 --- a/back +++ b/back @@ -1 +1 @@ -Subproject commit 2def0a489db10948c0a3674b874250e955a8d309 +Subproject commit 96ff9daae08f68f6032ca60110fb367488a982f7 diff --git a/docs/data/google_analytics/report_snapshot_20260101_20260314.csv b/docs/data/google_analytics/report_snapshot_20260101_20260314.csv deleted file mode 100644 index 1d5a6de..0000000 --- a/docs/data/google_analytics/report_snapshot_20260101_20260314.csv +++ /dev/null @@ -1,469 +0,0 @@ -# ---------------------------------------- -# レポートのスナップショット -# アカウント: Ippei Shimizu -# プロパティ: buzzbase.jp -# ---------------------------------------- -# -# 開始日: 20260101 -# 終了日: 20260314 -アクティブ ユーザー,新規ユーザー数,アクティブ ユーザーあたりの平均エンゲージメント時間,イベント数 -901,876,146.51831298557158,12836 - -# -# 開始日: 20260101 -# 終了日: 20260314 -ページ タイトルとスクリーン クラス,表示回数,アクティブ ユーザー,イベント数,直帰率 -BUZZ BASE | 野球の個人成績をランキング形式で共有できるアプリ - BUZZ BASE 野球の個人成績をランキング形式で共有できるアプリ,2916,486,4713,0.3048780487804878 -試合一覧 - BUZZ BASE,802,104,950,0.16498316498316498 -試合結果まとめ - BUZZ BASE,664,73,740,0.010638297872340425 -ログイン - BUZZ BASE,530,222,985,0.05898876404494382 -打撃成績を記録 - BUZZ BASE,424,71,476,0.012269938650306749 -BUZZ BASE|野球の個人成績を無料で記録・計算・共有できるアプリ - BUZZ BASE 野球の個人成績をランキング形式で共有できるアプリ,417,241,1070,0.3561643835616438 -投手成績を記録 - BUZZ BASE,406,66,433,0.013605442176870748 -グループ一覧 - BUZZ BASE,356,79,411,0.05223880597014925 -新規会員登録 - BUZZ BASE,312,183,593,0.058823529411764705 -野球ノート - BUZZ BASE,229,79,292,0.075 -ダッシュボード - BUZZ BASE,203,35,256,0.07526881720430108 -ユーザー名・ユーザーID登録 - BUZZ BASE,173,114,270,0.04065040650406504 -野球成績の計算方法&指標一覧|打率・防御率・OPSなど全29指標を解説 - BUZZ BASE,157,73,328,0.27472527472527475 -OPS計算ツール|出塁率・長打率・OPSを生データから一括自動計算 - BUZZ BASE,127,82,363,0.43564356435643564 -成績の算出方法 - BUZZ BASE,110,82,264,0.3627450980392157 -ユーザー検索 - BUZZ BASE,69,37,75,0.041666666666666664 -打率計算ツール|安打数と打数から打率を自動計算 - BUZZ BASE,65,33,89,0.02040816326530612 -お問い合わせ - BUZZ BASE,50,33,84,0.09090909090909091 -グループ新規作成,32,20,53,0.045454545454545456 -出塁率(OBP)計算ツール|安打・四球・死球から出塁率を自動計算 - BUZZ BASE,30,17,63,0.3333333333333333 -利用規約 - BUZZ BASE,30,22,38,0.08 -運営からのお知らせ - BUZZ BASE,26,26,41,0.038461538461538464 -長打率(SLG)計算ツール|塁打数と打数から長打率を自動計算 - BUZZ BASE,25,8,33,0 -ユーザーID未設定による不具合 - BUZZ BASE,24,14,27,0.06666666666666667 -K/BB計算ツール|奪三振と与四球からK/BBを自動計算 - BUZZ BASE,21,16,61,0.4090909090909091 -防御率(ERA)計算ツール|自責点と投球回から防御率を自動計算 - BUZZ BASE,20,6,38,0 -プライバシーポリシー - BUZZ BASE,19,8,30,0.07692307692307693 -WHIP計算ツール|与四球と被安打から WHIPを自動計算 - BUZZ BASE,11,9,23,0.18181818181818182 -シーズン管理 - BUZZ BASE,5,3,6,0 -K/9計算ツール|奪三振数と投球回からK/9を自動計算 - BUZZ BASE,4,4,9,0.2 -メンバー招待,4,1,8,0 -BB/9計算ツール|与四球と投球回からBB/9を自動計算 - BUZZ BASE,2,2,5,0.5 -メンバー一覧,1,1,2,0 - -# -# 開始日: 20260101 -# 終了日: 20260314 -ユーザーの最初の参照元 / メディア,アクティブ ユーザー -google / organic,481 -(direct) / (none),173 -yahoo / organic,152 -bing / organic,20 -runteq.jp / referral,17 -chatgpt.com / (not set),16 -github.com / referral,15 -t.co / referral,14 -service.smt.docomo.ne.jp / referral,5 -qiita.com / referral,4 -jariblog.online / referral,1 -l.instagram.com / referral,1 -mail.yahoo.co.jp / referral,1 -search.fenrir-inc.com / referral,1 - -# -# 開始日: 20260101 -# 終了日: 20260314 -セッションの参照元 / メディア,セッション -google / organic,811 -(direct) / (none),400 -yahoo / organic,180 -github.com / referral,57 -runteq.jp / referral,28 -bing / organic,24 -chatgpt.com / (not set),18 -t.co / referral,14 -service.smt.docomo.ne.jp / referral,11 -localhost / referral,4 -mail.yahoo.co.jp / referral,3 -qiita.com / referral,3 -jariblog.online / referral,1 -l.instagram.com / referral,1 -search.fenrir-inc.com / referral,1 -streamyard.com / referral,1 - -# -# 開始日: 20260101 -# 終了日: 20260314 -N 日目,new,returning -0000,5,2 -0001,4,1 -0002,3,1 -0003,8,3 -0004,5,0 -0005,5,1 -0006,4,0 -0007,10,0 -0008,5,5 -0009,5,3 -0010,9,4 -0011,16,3 -0012,14,5 -0013,10,4 -0014,3,4 -0015,9,1 -0016,7,2 -0017,9,3 -0018,15,1 -0019,5,3 -0020,5,3 -0021,7,1 -0022,4,1 -0023,10,1 -0024,3,1 -0025,9,3 -0026,20,3 -0027,8,3 -0028,6,4 -0029,7,2 -0030,6,2 -0031,10,3 -0032,7,6 -0033,9,5 -0034,5,2 -0035,12,4 -0036,0,4 -0037,5,3 -0038,14,4 -0039,9,6 -0040,9,4 -0041,10,6 -0042,9,1 -0043,6,2 -0044,8,2 -0045,20,8 -0046,13,9 -0047,10,4 -0048,5,5 -0049,4,3 -0050,7,3 -0051,9,6 -0052,9,5 -0053,14,6 -0054,24,9 -0055,11,8 -0056,14,3 -0057,11,4 -0058,18,6 -0059,8,9 -0060,13,7 -0061,16,6 -0062,16,5 -0063,13,5 -0064,21,6 -0065,40,14 -0066,47,21 -0067,38,17 -0068,34,7 -0069,34,8 -0070,30,7 -0071,33,16 -0072,17,9 - -# プラットフォーム上のアクティビティの比較 -# 開始日: 20260101 -# 終了日: 20260314 -プラットフォーム,キーイベント - - -# -# 開始日: 20260101 -# 終了日: 20260314 -市区町村,アクティブ ユーザー -Osaka,73 -Shinjuku City,52 -Fukuoka,39 -Sapporo,34 -Kobe,27 -Shibuya,26 -Yokohama,26 -Minato City,25 -Itabashi City,22 -Nagoya,20 -Kyoto,13 -Chiyoda City,12 -Sendai,11 -Setagaya City,11 -Tehran,11 -Koto City,10 -Chiba,9 -Kawasaki,9 -Shizuoka,9 -Columbus,8 -Hiroshima,8 -Kitakyushu,8 -Niigata,8 -Saitama,8 -Chuo City,7 -Shinagawa City,7 -Kumamoto,6 -Mito,6 -Nerima City,6 -Tsukuba,6 -Yokkaichi,6 -Moriya,5 -Nakano City,5 -Otsu,5 -Sakai,5 -Wakayama,5 -Yokosuka,5 -Butzbach,4 -Edogawa City,4 -Eniwa,4 -Fuchu,4 -Fukuyama,4 -Hachioji,4 -Kagoshima,4 -Kawaguchi,4 -Miyazaki,4 -Nagano,4 -Suita,4 -Tachikawa,4 -Takamatsu,4 -Toyama,4 -Toyonaka,4 -Bunkyo City,3 -Funabashi,3 -Gifu,3 -Hamamatsu,3 -Higashiosaka,3 -Himeji,3 -Hirakata,3 -Ichikawa,3 -Kurashiki,3 -Lanzhou,3 -Misawa,3 -Naha,3 -Okayama,3 -Ota City,3 -Sagamihara,3 -Suginami City,3 -Suzuka,3 -Taito City,3 -Tokushima,3 -Toshima City,3 -Utsunomiya,3 -Adachi City,2 -Arakawa City,2 -Arida,2 -Aspen,2 -Bissen,2 -Chichibu,2 -Chigasaki,2 -Fujisawa,2 -Higashihiroshima,2 -Hitachiomiya,2 -Hyuga,2 -Ibaraki,2 -Inagi,2 -Ishinomaki,2 -Itami,2 -Kakogawa,2 -Kashiwa,2 -Kasukabe,2 -Kochi,2 -Komaki,2 -Kure,2 -Kurume,2 -Maebashi,2 -Matsudo,2 -Matsue,2 -Matsuyama,2 -Miura,2 -Musashino,2 -Narashino,2 -Nishitokyo,2 -Odawara,2 -Owariasahi,2 -Oyama,2 -Seki,2 -Shibata,2 -Shimonoseki,2 -Shiraz,2 -Singapore,2 -Sumida City,2 -Tajimi,2 -Takaoka,2 -Toba,2 -Tokorozawa,2 -Toyohashi,2 -Uda,2 -Ueda,2 -Uwajima,2 -Wako,2 -Yamagata,2 -Yamatokoriyama,2 -Yonago,2 -Ageo,1 -Aira,1 -Ami,1 -Anjo,1 -Anklam,1 -Asahikawa,1 -Ashburn,1 -Athens,1 -Babol,1 -Beppu,1 -Boardman,1 -Braunschweig,1 -Bremerhaven,1 -Chikugo,1 -Chino,1 -Chita,1 -Chitose,1 -Council Bluffs,1 -Daisen,1 -Daito,1 -Dazaifu,1 -Dublin,1 -Ena,1 -Fuji,1 -Fujieda,1 -Fukui,1 -Gobo,1 -Gujo,1 -Gyoda,1 -Hannan,1 -Higashimurayama,1 -Higashiyamato,1 -Hikari,1 -Hirosaki,1 -Hitachi,1 -Hitachinaka,1 -Hokuto,1 -Honjo,1 -Iizuka,1 -Isesaki,1 -Iwaizumi,1 -Iwaki,1 -Izumi,1 -Izumo,1 -Joetsu,1 -Kaizuka,1 -Kakamigahara,1 -Kamakura,1 -Kanazawa,1 -Kanie,1 -Kannami,1 -Kanonji,1 -Kanzaki,1 -Kariya,1 -Karuizawa,1 -Kasugai,1 -Katsushika City,1 -Kazo,1 -Kikuchi,1 -Kita City,1 -Kitahiroshima,1 -Kitanagoya,1 -Kiyose,1 -Kodaira,1 -Kofu,1 -Koga,1 -Konan,1 -Konosu,1 -Kota,1 -Kumano,1 -Kusatsu,1 -Kushiro,1 -Landsberg,1 -Lille,1 -Lincoln,1 -London,1 -Machida,1 -Mannheim,1 -Matsumae,1 -Matsumoto,1 -Matsusaka,1 -Meguro City,1 -Mihama,1 -Miho,1 -Minami-Alps,1 -Minamiminowa,1 -Misato,1 -Miyaki,1 -Miyakojima,1 -Miyakonojo,1 -Miyoshi,1 -Mizunami,1 -Mobara,1 -Mori,1 -Munakata,1 -Munich,1 -Muroran,1 -Muroto,1 -Nagai,1 -Nagaoka,1 -Nagasaki,1 -Nago,1 -Nantan,1 -Nara,1 -Narita,1 -Nayoro,1 -New York,1 -Niihama,1 -Niiza,1 -Nishinomiya,1 -Nuremberg,1 -Oberasbach,1 -Oberdorla,1 -Obihiro,1 -Ogori,1 -Oita,1 -Okazaki,1 -Okinawa,1 -Ome,1 -Omihachiman,1 -Omsk,1 -Omuta,1 -Ozu,1 -Panama City,1 -Saint Petersburg,1 -San Francisco,1 -Shihoro,1 -Shikokuchuo,1 -Shimosuwa,1 -Shinshiro,1 -Shiso,1 -Soka,1 -Taka,1 -Takarazuka,1 -Takasaki,1 -Takatsuki,1 -Tamamura,1 -Tatebayashi,1 -Tateyama,1 -Tendo,1 -Toda,1 -Togane,1 -Toin,1 -Tono City,1 -Toronto,1 -Tottori,1 -Towada,1 -Tsu,1 -Tsubata,1 -Tsuchiura,1 -Tsukubamirai,1 -Tsurugashima,1 -Tsuruoka,1 -Ube,1 -Uji,1 -Unzen,1 -Urakawa,1 -Yaizu,1 -Yakhroma,1 -Yanagawa,1 -Yasugi,1 -Yurihonjo,1 - -# -# 開始日: 20260101 -# 終了日: 20260314 -オーディエンス名,アクティブ ユーザー -All Users,901 \ No newline at end of file diff --git "a/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\343\202\257\343\202\250\343\203\252.csv" "b/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\343\202\257\343\202\250\343\203\252.csv" deleted file mode 100644 index c3c475e..0000000 --- "a/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\343\202\257\343\202\250\343\203\252.csv" +++ /dev/null @@ -1,460 +0,0 @@ -上位のクエリ,クリック数,表示回数,CTR,掲載順位 -野球 個人成績 アプリ 無料,73,496,14.72%,3.69 -buzz base,31,67,46.27%,1.61 -ops 計算,22,1451,1.52%,4.83 -buzzbase,17,34,50%,3.09 -野球 成績 アプリ,11,167,6.59%,5.93 -出塁率 計算,9,465,1.94%,3.71 -野球計算機,9,43,20.93%,3.42 -打率計算,8,1022,0.78%,7.94 -野球ノート アプリ,8,125,6.4%,10.18 -打率 計算,8,115,6.96%,11.07 -野球 個人成績 アプリ,8,64,12.5%,3.67 -打撃成績 計算,8,48,16.67%,4.92 -打撃成績 アプリ,7,40,17.5%,3.2 -打率計算アプリ,6,214,2.8%,6.97 -野球 打率 アプリ 無料,6,77,7.79%,4.12 -長打率 計算,5,945,0.53%,4.61 -打率計算 アプリ,5,100,5%,5.23 -野球成績管理アプリ,5,77,6.49%,6.09 -野球 成績 計算,5,77,6.49%,7.94 -野球成績アプリ,4,155,2.58%,5.86 -野球 打率 アプリ,4,77,5.19%,6.32 -k/bb 計算,4,11,36.36%,2.27 -ops計算,3,160,1.88%,5.27 -野球 指標 一覧,3,148,2.03%,3.2 -ops 計算式,3,40,7.5%,5.7 -ops と は計算,2,77,2.6%,6.61 -打率アプリ,2,67,2.99%,5.3 -野球 ops 計算,2,65,3.08%,2.78 -ops 計算 ツール,2,30,6.67%,2.93 -ops計算機,2,14,14.29%,3.79 -打者成績 計算,2,13,15.38%,6 -k/bb,1,173,0.58%,5.68 -ops 計算方法,1,131,0.76%,7.04 -出塁率計算,1,96,1.04%,3.44 -野球成績計算,1,67,1.49%,5.96 -打率計算 サイト,1,59,1.69%,5.64 -打率計算機,1,57,1.75%,5.61 -長打率計算,1,57,1.75%,6.95 -長 打率 計算,1,54,1.85%,6.93 -野球打率アプリ,1,46,2.17%,6.09 -打率 アプリ,1,37,2.7%,4.62 -野球スコア アプリ 個人成績,1,30,3.33%,9.2 -出塁率 計算方法,1,24,4.17%,4.08 -打率 計算サイト,1,22,4.55%,6.27 -bb/9 野球,1,17,5.88%,3.29 -成績管理アプリ 無料,1,17,5.88%,6.76 -ops出し方,1,16,6.25%,5.81 -野球出塁率計算,1,14,7.14%,3.57 -野球指標 一覧,1,14,7.14%,3.64 -投手成績 計算,1,14,7.14%,7 -野球スコアアプリ 個人成績,1,12,8.33%,12.08 -野球 指標 計算,1,6,16.67%,7.17 -とうしゅ,1,1,100%,1 -野球 失点とは,0,308,0%,1 -防御率,0,216,0%,1.12 -オーピーエス 計算,0,152,0%,4.57 -打率の出し方,0,129,0%,4.5 -打率とは,0,114,0%,1.01 -野球記録計算,0,102,0%,6.12 -打率 計算方法,0,102,0%,10.45 -k/bbとは,0,83,0%,5.52 -自責点とは,0,68,0%,1 -長打率 計算方法,0,68,0%,7.44 -野球 打率 計算,0,67,0%,10.93 -防御率とは,0,65,0%,1 -被安打率 計算,0,58,0%,9.38 -打率の計算,0,57,0%,4 -失点とは 野球,0,55,0%,1 -失点と は 野球,0,52,0%,1.04 -打率,0,52,0%,1.17 -k/bb 目安,0,51,0%,3.33 -超打率 計算,0,45,0%,5.29 -防御率計算,0,43,0%,20.28 -抑えのピッチャー,0,39,0%,1 -k-bbパーセント,0,37,0%,6.46 -打率計算方法,0,36,0%,8.42 -野球 長打率 計算,0,34,0%,4.76 -ops計算方法,0,34,0%,6.41 -長打率 出し方,0,33,0%,1.55 -obp 野球,0,33,0%,8.82 -野球失点とは,0,31,0%,1 -成績計算,0,31,0%,7.23 -成績,0,31,0%,43.87 -k-bb%,0,30,0%,5.53 -三振率,0,30,0%,6.3 -野球成績 アプリ,0,29,0%,6.24 -k/bb,0,28,0%,6.5 -被本塁打とは,0,27,0%,1 -野球 成績 見方,0,27,0%,3.81 -打率求め方,0,26,0%,4.58 -野球の失点とは,0,25,0%,1 -野球の打率の出し方,0,25,0%,6.4 -抑え投手,0,24,0%,1 -打率 とは,0,22,0%,1 -打率出し方,0,22,0%,3.73 -超打率 計算方法,0,22,0%,7.09 -k-bb 野球,0,21,0%,5.14 -塁打数 計算,0,21,0%,9.1 -k-bb,0,20,0%,5.1 -抑えピッチャー,0,19,0%,1 -野球 失点率 計算,0,19,0%,5 -打率 求め方,0,19,0%,6.63 -打率計算の仕方,0,18,0%,1 -野球 打率とは,0,18,0%,1 -ops計算式,0,18,0%,5.78 -打数 計算,0,18,0%,10.11 -野球の打率,0,17,0%,1 -野球スコアの付け方,0,17,0%,1 -打率の計算方法,0,17,0%,4.47 -野球 obp,0,17,0%,8.65 -野球打率の出し方,0,17,0%,9.41 -与四球率 計算,0,16,0%,6.62 -野球打率計算,0,16,0%,7.88 -obp 出塁率,0,16,0%,8.06 -野球 指標,0,16,0%,10 -被本塁打率 計算,0,16,0%,11.12 -wbc 失点 率 計算 方法,0,15,0%,1 -野球打率とは,0,15,0%,1 -打率の求め方,0,15,0%,4.93 -防御率 計算 アプリ,0,15,0%,8.2 -防御率 計算方法,0,15,0%,16.13 -イニングとは,0,14,0%,1 -出塁率 目安,0,14,0%,10.86 -野球 出塁率 計算,0,13,0%,3.23 -野球 ops 計算方法,0,13,0%,5.08 -ops 算出方法,0,13,0%,5.62 -野球 防御率 計算,0,13,0%,7.31 -野球 打率計算,0,13,0%,12.23 -長打率 計算式,0,13,0%,13.54 -与四死球率,0,12,0%,1 -失点率とは,0,12,0%,1.67 -打率 出し方,0,12,0%,2.58 -出塁率 出し方,0,12,0%,3.33 -野球 個人成績,0,12,0%,8.75 -ops 野球 指標,0,12,0%,13.17 -防御率 計算,0,12,0%,15.83 -k-bb 目安,0,11,0%,2.82 -出塁率 求め方,0,11,0%,2.82 -野球 得点率 計算,0,11,0%,3.73 -ops 出し方,0,11,0%,6.45 -bb 野球,0,10,0%,1 -野球 bb,0,10,0%,1 -三振率 計算 野手,0,10,0%,1.1 -ops求め方,0,10,0%,5.5 -野球アプリ 無料,0,10,0%,8.1 -投手 勝率 計算,0,10,0%,11.3 -ops 目安,0,10,0%,11.7 -打率 出塁率,0,9,0%,1 -失点とは,0,9,0%,2 -投手 成績 指標,0,9,0%,3.78 -k bb,0,9,0%,4.78 -野球 成績管理アプリ,0,9,0%,5.67 -kbb 目安,0,9,0%,7.11 -野球 防御率 計算方法,0,9,0%,8 -被打率 計算,0,9,0%,10.67 -k-bb 平均,0,8,0%,1 -イニング数とは,0,8,0%,1 -抑え ピッチャー,0,8,0%,1 -野球チーム管理アプリ,0,8,0%,1.25 -出塁率求め方,0,8,0%,1.62 -防御 率 計算,0,8,0%,49.62 -ピッチャー 抑え,0,7,0%,1 -被打率,0,7,0%,1 -出塁率出し方,0,7,0%,3.14 -オーピーエス計算,0,7,0%,4.43 -出塁率 計算式,0,7,0%,4.57 -投手 指標 一覧,0,7,0%,5.14 -打率計算サイト,0,7,0%,6.57 -opsの計算方法,0,7,0%,7 -kbb 野球 目安,0,7,0%,7.86 -野球ノート 無料,0,7,0%,7.86 -個人成績表,0,7,0%,8.14 -野球 打率 計算方法,0,7,0%,9.57 -防御率の計算方法,0,7,0%,9.57 -kbb 野球,0,6,0%,1 -バッティングのアベレージ,0,6,0%,1 -失点率,0,6,0%,1 -犠牲フライ 出塁率,0,6,0%,1 -被本塁打,0,6,0%,1 -野球 投手 指標,0,6,0%,1 -bb/k 野手,0,6,0%,1.33 -出塁率とは,0,6,0%,1.83 -打撃指標,0,6,0%,2 -安打とは,0,6,0%,2.67 -出塁率の出し方,0,6,0%,3.67 -k/bb 野球,0,6,0%,6 -四球率 計算,0,6,0%,6.17 -打率 計算式,0,6,0%,6.5 -四死球率 計算,0,6,0%,9.67 -打率の計算の仕方,0,6,0%,10 -bb/9 計算,0,6,0%,11.83 -三振率 計算,0,6,0%,15.83 -bb/k,0,5,0%,1 -kbbとは,0,5,0%,1 -opsランキング 日本,0,5,0%,1 -セーブ 野球,0,5,0%,1 -投手 指標,0,5,0%,1 -野球 ip,0,5,0%,1 -野球 失点,0,5,0%,1 -失点,0,5,0%,2.6 -野球スコア 集計 アプリ,0,5,0%,3.4 -base buzz,0,5,0%,5.2 -ops 求め方,0,5,0%,6.2 -bb-9,0,5,0%,7.2 -野球防御率計算,0,5,0%,9 -ops 野球 目安,0,5,0%,10 -ops,0,5,0%,16.8 -失点 自責点,0,4,0%,1 -抑え 投手,0,4,0%,1 -野球 bb/k,0,4,0%,1 -野球 出塁率,0,4,0%,1 -長打率 求め方,0,4,0%,1 -wbc 失点率とは,0,4,0%,1.5 -k bb 目安,0,4,0%,2.25 -obp野球,0,4,0%,3.75 -成績 計算,0,4,0%,6.25 -長打率の計算方法,0,4,0%,7 -野球個人成績,0,4,0%,8 -長 打率 ops,0,4,0%,9.75 -与四死球率 計算,0,4,0%,11.25 -1アウトあたりの失点数,0,3,0%,1 -bb%,0,3,0%,1 -bb/k 目安,0,3,0%,1 -k/bb 平均,0,3,0%,1 -wbc 失点率 計算方法,0,3,0%,1 -プロ野球 kbb,0,3,0%,1 -打率とは 簡単に,0,3,0%,1 -投球回数,0,3,0%,1 -被安打とは,0,3,0%,1 -野球 war とは,0,3,0%,1 -野球 自責点,0,3,0%,1 -野球 自責点とは,0,3,0%,1 -野球の回,0,3,0%,1 -長打率求め方,0,3,0%,1 -制球力,0,3,0%,1.67 -失点 野球,0,3,0%,1.67 -野球 ops とは,0,3,0%,1.67 -野球 打者 指標,0,3,0%,2 -野球 失点率とは,0,3,0%,2.33 -出塁率,0,3,0%,3 -k-bb とは,0,3,0%,8 -野球指標,0,3,0%,8.33 -防御率計算機,0,3,0%,8.67 -k/bb 読み方,0,3,0%,9.67 -野球 失点率 計算方法,0,3,0%,9.67 -失点率 計算方法,0,3,0%,12.33 -長打率 ops,0,3,0%,12.33 -自責点 計算,0,3,0%,19.67 -奪三振率 計算,0,3,0%,35 -野球 勝率 計算,0,3,0%,55.67 -bbk 野球,0,2,0%,1 -bb野球,0,2,0%,1 -wbc 勝ち投手条件,0,2,0%,1 -wbc 失点率計算,0,2,0%,1 -けーびーびーとは,0,2,0%,1 -バッティングアベレージとは,0,2,0%,1 -ピッチャー 野球,0,2,0%,1 -出塁率5割,0,2,0%,1 -失点率 野球,0,2,0%,1 -失点率計算,0,2,0%,1 -好打者とは,0,2,0%,1 -打率表し方,0,2,0%,1 -投手 勝率,0,2,0%,1 -投手 本塁打 ランキング,0,2,0%,1 -相手のエラー 打点,0,2,0%,1 -野球 1回で何人打つ,0,2,0%,1 -野球 spとは,0,2,0%,1 -野球 ウィップ,0,2,0%,1 -野球 スコア hp,0,2,0%,1 -野球spとは,0,2,0%,1 -野球で1 アウト,0,2,0%,1 -野球で1アウト,0,2,0%,1 -野球の防御率の算出に必要な2つの指標は実績点と何,0,2,0%,1 -三振率 打者,0,2,0%,1.5 -三振率 打者 計算,0,2,0%,1.5 -打者 指標,0,2,0%,1.5 -無得点負け,0,2,0%,1.5 -計算方法は?,0,2,0%,1.5 -k-bb パーセント ランキング,0,2,0%,2 -左打者 右手 有鉤骨 骨折 復帰 打撃 成績,0,2,0%,2 -敗戦投手,0,2,0%,2 -野球 指標一覧,0,2,0%,2 -野球スコアk,0,2,0%,2 -計算方法,0,2,0%,2.5 -wbc 失点とは,0,2,0%,3 -長打率とは,0,2,0%,3 -野球 計算,0,2,0%,3.5 -リリーフ防御率,0,2,0%,4 -ops 計算機,0,2,0%,5 -bb/9,0,2,0%,5.5 -成績計算サイト,0,2,0%,5.5 -四球率 打者,0,2,0%,6 -k bb 野球,0,2,0%,7 -ops 優秀,0,2,0%,10 -野球成績,0,2,0%,11 -防御率計算 ツール,0,2,0%,11 -野球 成績,0,2,0%,11.5 -長打率 目安,0,2,0%,12 -防御率 出し方,0,2,0%,54 -勝率 計算 方法,0,2,0%,62 -勝率 計算方法,0,2,0%,63.5 -1アウトあたりの失点数が少ないチーム,0,1,0%,1 -bb% 野球,0,1,0%,1 -bb/9 目安,0,1,0%,1 -bbパーセント,0,1,0%,1 -ip 野球 スコア,0,1,0%,1 -k 野球 意味,0,1,0%,1 -k%-bb%,0,1,0%,1 -kast率とは,0,1,0%,1 -kパーセント 野球,0,1,0%,1 -ops 1 超え 意味,0,1,0%,1 -ops 野球,0,1,0%,1 -opsとは,0,1,0%,1 -opsは?,0,1,0%,1 -warとは 野球,0,1,0%,1 -wbc 失点率 計算,0,1,0%,1 -wbc 失点率の計算方法,0,1,0%,1 -wpaとは 野球,0,1,0%,1 -アプリなの?,0,1,0%,1 -サカつく 活動量,0,1,0%,1 -シャドバ 勝率 確認方法,0,1,0%,1 -バッティングアベレージ とは,0,1,0%,1 -バレーボール 効果率 計算,0,1,0%,1 -プロ野球 opsとは,0,1,0%,1 -三振 k,0,1,0%,1 -三振 k 意味,0,1,0%,1 -与四球,0,1,0%,1 -先発とは 野球,0,1,0%,1 -出塁とは,0,1,0%,1 -勝ち投手,0,1,0%,1 -勝ち星とは,0,1,0%,1 -四死球とは,0,1,0%,1 -四死球率,0,1,0%,1 -失点率 計算式,0,1,0%,1 -失点率とは 野球,0,1,0%,1 -奪三振,0,1,0%,1 -奪三振 k,0,1,0%,1 -好投とは,0,1,0%,1 -守備アウト数とは,0,1,0%,1 -安打率,0,1,0%,1 -得失点率 計算,0,1,0%,1 -戦績管理ツール,0,1,0%,1 -打ってよし 投げてよし,0,1,0%,1 -打席と打数の違い,0,1,0%,1 -打撃,0,1,0%,1 -打数 打席 違い,0,1,0%,1 -打点 得点 違い,0,1,0%,1 -打率 意味,0,1,0%,1 -打率 見方,0,1,0%,1 -打率とは 野球,0,1,0%,1 -打率意味,0,1,0%,1 -打率計算式,0,1,0%,1 -投ゴロ,0,1,0%,1 -投手指標,0,1,0%,1 -援護率 計算,0,1,0%,1 -本塁打とは,0,1,0%,1 -無料なの?,0,1,0%,1 -犠飛,0,1,0%,1 -自責点,0,1,0%,1 -自責点 とは,0,1,0%,1 -自責点とは 野球,0,1,0%,1 -被安打,0,1,0%,1 -被安打率,0,1,0%,1 -被弾 野球,0,1,0%,1 -貢献度とは,0,1,0%,1 -選球眼 指標,0,1,0%,1 -野球 1アウト,0,1,0%,1 -野球 k 意味,0,1,0%,1 -野球 obpとは,0,1,0%,1 -野球 opsとは,0,1,0%,1 -野球 の回,0,1,0%,1 -野球 イニングとは,0,1,0%,1 -野球 スコアの付け方,0,1,0%,1 -野球 セーブ,0,1,0%,1 -野球 三振 k,0,1,0%,1 -野球 勝率,0,1,0%,1 -野球 防御率 見方,0,1,0%,1 -野球 防御率とは,0,1,0%,1 -野球warとは,0,1,0%,1 -野球のwarとは,0,1,0%,1 -野球の投手,0,1,0%,1 -野球スコアhpとは,0,1,0%,1 -野球セーブとは,0,1,0%,1 -野球ヒットとは,0,1,0%,1 -野球ピッチャー,0,1,0%,1 -野球投手,0,1,0%,1 -野球自責点とは,0,1,0%,1 -長打率,0,1,0%,1 -長打率出し方,0,1,0%,1 -防御率 とは,0,1,0%,1 -3イニングとは,0,1,0%,2 -k/9 野球,0,1,0%,2 -kbb,0,1,0%,2 -プロ野球 三振率,0,1,0%,2 -プロ野球 出塁率,0,1,0%,2 -一覧,0,1,0%,2 -勝ち数,0,1,0%,2 -失点 とは,0,1,0%,2 -打席,0,1,0%,2 -打者,0,1,0%,2 -投手,0,1,0%,2 -無失点,0,1,0%,2 -率,0,1,0%,2 -算出方法は?,0,1,0%,2 -計算式を教えて,0,1,0%,2 -野球 データ アプリ,0,1,0%,2 -野球 データ エクセル 無料,0,1,0%,2 -野球 失,0,1,0%,2 -bリーグ 貢献度,0,1,0%,3 -ops ランキング,0,1,0%,3 -どう計算するの?,0,1,0%,3 -プロスピa,0,1,0%,3 -成績 計算 サイト,0,1,0%,3 -打線とは,0,1,0%,3 -投球回,0,1,0%,3 -残塁とは,0,1,0%,3 -計算方法は,0,1,0%,3 -野球ops,0,1,0%,3 -野球のスコアの見方,0,1,0%,3 -野球個人成績 アプリ,0,1,0%,3 -野球失点率とは,0,1,0%,3 -野球無料アプリ,0,1,0%,3 -ベースボールマネージャー,0,1,0%,4 -リリーフ 防御率,0,1,0%,4 -ワンナウト 野球,0,1,0%,4 -出率計算,0,1,0%,4 -野球スコアアプリ 初心者 無料,0,1,0%,4 -防御率 計算式,0,1,0%,4 -k bb ratio,0,1,0%,5 -k/bb%ランキング,0,1,0%,5 -○○投手,0,1,0%,5 -サイトを教えてください,0,1,0%,5 -スコア計算方法,0,1,0%,5 -野球 アプリ スコア,0,1,0%,5 -野球用語,0,1,0%,5 -auto ops,0,1,0%,7 -buzz マイページ,0,1,0%,8 -個人成績,0,1,0%,8 -個人記録,0,1,0%,8 -score base,0,1,0%,9 -お願いします,0,1,0%,9 -野球アプリ無料,0,1,0%,9 -本塁打率 計算,0,1,0%,12 -野球失点率計算,0,1,0%,12 -与 四球 率 計算,0,1,0%,13 -野球計算,0,1,0%,14 -ops 指標,0,1,0%,18 -データ系,0,1,0%,18 -野球 kbb,0,1,0%,32 -本塁打 率 計算,0,1,0%,40 -打率 計算 方法,0,1,0%,47 -打率 計算機,0,1,0%,48 -防御 率 計算 方法,0,1,0%,52 -防御率出し方,0,1,0%,53 -野球勝率計算,0,1,0%,54 -勝率の出し方,0,1,0%,58 -プロ野球 勝率 投手 計算方法,0,1,0%,80 -勝率の計算,0,1,0%,85 -プロ野球 勝率 計算,0,1,0%,89 \ No newline at end of file diff --git "a/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\343\203\207\343\203\220\343\202\244\343\202\271.csv" "b/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\343\203\207\343\203\220\343\202\244\343\202\271.csv" deleted file mode 100644 index 6011d44..0000000 --- "a/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\343\203\207\343\203\220\343\202\244\343\202\271.csv" +++ /dev/null @@ -1,4 +0,0 @@ -デバイス,クリック数,表示回数,CTR,掲載順位 -モバイル,463,15389,3.01%,4.48 -PC,116,3111,3.73%,8.98 -タブレット,12,348,3.45%,4.72 \ No newline at end of file diff --git "a/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\343\203\225\343\202\243\343\203\253\343\202\277.csv" "b/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\343\203\225\343\202\243\343\203\253\343\202\277.csv" deleted file mode 100644 index 92853b6..0000000 --- "a/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\343\203\225\343\202\243\343\203\253\343\202\277.csv" +++ /dev/null @@ -1,3 +0,0 @@ -フィルタ,値 -検索タイプ,ウェブ -日付,2026/01/01-2026/03/14 \ No newline at end of file diff --git "a/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\343\203\232\343\203\274\343\202\270.csv" "b/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\343\203\232\343\203\274\343\202\270.csv" deleted file mode 100644 index 1f38ccf..0000000 --- "a/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\343\203\232\343\203\274\343\202\270.csv" +++ /dev/null @@ -1,18 +0,0 @@ -上位のページ,クリック数,表示回数,CTR,掲載順位 -https://buzzbase.jp/,373,4928,7.57%,4.78 -https://buzzbase.jp/calculation-of-grades,115,8129,1.41%,5.47 -https://buzzbase.jp/tools/ops,61,4264,1.43%,5.62 -https://buzzbase.jp/tools/k-bb,16,720,2.22%,5.11 -https://buzzbase.jp/tools/obp,11,947,1.16%,3.76 -https://buzzbase.jp/note/new,10,187,5.35%,8.67 -https://buzzbase.jp/mypage/sami,3,117,2.56%,3.43 -https://buzzbase.jp/account-deletion,2,86,2.33%,2.7 -https://buzzbase.jp/mypage/tomo_49,2,52,3.85%,2.65 -https://buzzbase.jp/signup,1,78,1.28%,2.67 -https://buzzbase.jp/tools/bb-9,1,73,1.37%,4.32 -https://buzzbase.jp/termsofservice,1,64,1.56%,5.47 -https://buzzbase.jp/contact,0,27,0%,1.63 -https://buzzbase.jp/signup?auth_required=true,0,17,0%,5.18 -https://buzzbase.jp/privacypolicy,0,11,0%,3 -https://buzzbase.jp/mypage/big_boy71/following,0,1,0%,9 -https://buzzbase.jp/mypage/sami/following,0,1,0%,10 \ No newline at end of file diff --git "a/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\345\233\275.csv" "b/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\345\233\275.csv" deleted file mode 100644 index 5c80f4b..0000000 --- "a/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\345\233\275.csv" +++ /dev/null @@ -1,63 +0,0 @@ -国,クリック数,表示回数,CTR,掲載順位 -日本,588,18412,3.19%,5.18 -パナマ,2,6,33.33%,3.17 -米国,1,181,0.55%,7.97 -スペイン,0,18,0%,6.28 -ブラジル,0,16,0%,7.5 -オーストラリア,0,15,0%,5.2 -カナダ,0,14,0%,5 -イギリス,0,14,0%,7.36 -台湾,0,13,0%,4.85 -フランス,0,10,0%,8.6 -メキシコ,0,9,0%,5.89 -韓国,0,9,0%,6 -中国,0,9,0%,6.67 -インド,0,9,0%,8.11 -ポーランド,0,8,0%,4.62 -インドネシア,0,8,0%,7.75 -マレーシア,0,8,0%,8.12 -タイ,0,6,0%,4.5 -イタリア,0,6,0%,5.33 -アルゼンチン,0,6,0%,9.5 -ベトナム,0,5,0%,3.2 -香港,0,4,0%,3 -シンガポール,0,3,0%,1.67 -ウクライナ,0,3,0%,5.67 -オランダ,0,3,0%,6 -バングラディッシュ,0,3,0%,8.33 -ドイツ,0,3,0%,9.67 -コロンビア,0,3,0%,11 -フィリピン,0,3,0%,12 -ポルトガル,0,2,0%,3.5 -コスタリカ,0,2,0%,4 -ハンガリー,0,2,0%,4 -ニュージーランド,0,2,0%,4.5 -トルコ,0,2,0%,5 -イスラエル,0,2,0%,7 -ネパール,0,2,0%,13 -プエルトリコ,0,2,0%,17 -ベルギー,0,1,0%,1 -ベラルーシ,0,1,0%,1 -エチオピア,0,1,0%,1 -カザフスタン,0,1,0%,1 -ニカラグア,0,1,0%,1 -ホンジュラス,0,1,0%,2 -セネガル,0,1,0%,2 -チェキア,0,1,0%,3 -ペルー,0,1,0%,5 -アラブ首長国連邦,0,1,0%,8 -デンマーク,0,1,0%,8 -クウェート,0,1,0%,8 -スリランカ,0,1,0%,8 -モロッコ,0,1,0%,8 -マルタ,0,1,0%,8 -ロシア,0,1,0%,9 -ウルグアイ,0,1,0%,9 -チリ,0,1,0%,10 -エクアドル,0,1,0%,10 -アルバニア,0,1,0%,11 -アイルランド,0,1,0%,12 -ルーマニア,0,1,0%,14 -スイス連邦,0,1,0%,15 -アゼルバイジャン,0,1,0%,22 -マリ,0,1,0%,25 \ No newline at end of file diff --git "a/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\345\271\263\345\235\207\350\252\255\343\201\277\350\276\274\343\201\277\346\231\202\351\226\223\343\201\256\343\203\201\343\203\243\343\203\274\343\203\210.csv" "b/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\345\271\263\345\235\207\350\252\255\343\201\277\350\276\274\343\201\277\346\231\202\351\226\223\343\201\256\343\203\201\343\203\243\343\203\274\343\203\210.csv" deleted file mode 100644 index 4ae0e9a..0000000 --- "a/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\345\271\263\345\235\207\350\252\255\343\201\277\350\276\274\343\201\277\346\231\202\351\226\223\343\201\256\343\203\201\343\203\243\343\203\274\343\203\210.csv" +++ /dev/null @@ -1,74 +0,0 @@ -日付,クリック数,表示回数,CTR,掲載順位 -2026-01-01,6,100,6%,7.2 -2026-01-02,5,77,6.49%,7.6 -2026-01-03,1,74,1.35%,8.3 -2026-01-04,4,85,4.71%,4.6 -2026-01-05,2,127,1.57%,4.2 -2026-01-06,4,125,3.2%,5.5 -2026-01-07,4,105,3.81%,5.7 -2026-01-08,2,83,2.41%,5.4 -2026-01-09,3,74,4.05%,5.9 -2026-01-10,2,130,1.54%,5.5 -2026-01-11,7,153,4.58%,7 -2026-01-12,8,167,4.79%,5.1 -2026-01-13,4,111,3.6%,5 -2026-01-14,4,107,3.74%,6.9 -2026-01-15,3,87,3.45%,5.8 -2026-01-16,5,114,4.39%,5.9 -2026-01-17,9,180,5%,4.4 -2026-01-18,8,141,5.67%,5.2 -2026-01-19,3,109,2.75%,4.9 -2026-01-20,3,88,3.41%,5 -2026-01-21,6,78,7.69%,5.4 -2026-01-22,4,108,3.7%,4.7 -2026-01-23,3,127,2.36%,4.6 -2026-01-24,1,120,0.83%,4.5 -2026-01-25,5,134,3.73%,5 -2026-01-26,4,118,3.39%,6.6 -2026-01-27,6,96,6.25%,5.3 -2026-01-28,4,96,4.17%,4.9 -2026-01-29,4,76,5.26%,6.3 -2026-01-30,3,99,3.03%,6.3 -2026-01-31,7,123,5.69%,7.6 -2026-02-01,3,139,2.16%,7.2 -2026-02-02,3,128,2.34%,7.8 -2026-02-03,6,122,4.92%,6.3 -2026-02-04,2,90,2.22%,5.5 -2026-02-05,8,108,7.41%,6.6 -2026-02-06,0,99,0%,9.7 -2026-02-07,5,123,4.07%,5.9 -2026-02-08,5,119,4.2%,7.3 -2026-02-09,2,91,2.2%,4.3 -2026-02-10,9,121,7.44%,6.1 -2026-02-11,2,105,1.9%,5.2 -2026-02-12,5,84,5.95%,8.2 -2026-02-13,3,98,3.06%,5.9 -2026-02-14,9,177,5.08%,4.5 -2026-02-15,23,231,9.96%,6 -2026-02-16,8,181,4.42%,6.4 -2026-02-17,2,108,1.85%,4.8 -2026-02-18,1,83,1.2%,4.6 -2026-02-19,7,100,7%,6 -2026-02-20,3,100,3%,5.2 -2026-02-21,3,110,2.73%,6.5 -2026-02-22,8,197,4.06%,4.8 -2026-02-23,9,144,6.25%,5.1 -2026-02-24,11,120,9.17%,5 -2026-02-25,4,95,4.21%,4.3 -2026-02-26,6,121,4.96%,5.3 -2026-02-27,2,140,1.43%,4.5 -2026-02-28,13,154,8.44%,4.5 -2026-03-01,5,102,4.9%,5.2 -2026-03-02,11,171,6.43%,4.8 -2026-03-03,13,162,8.02%,5 -2026-03-04,10,204,4.9%,5 -2026-03-05,10,191,5.24%,5 -2026-03-06,20,584,3.42%,5.1 -2026-03-07,29,1992,1.46%,5.2 -2026-03-08,41,2553,1.61%,4.4 -2026-03-09,33,1366,2.42%,5 -2026-03-10,30,1337,2.24%,5.4 -2026-03-11,32,1129,2.83%,4.4 -2026-03-12,40,1013,3.95%,5.2 -2026-03-13,20,974,2.05%,5.1 -2026-03-14,1,170,0.59%,6.9 \ No newline at end of file diff --git "a/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\346\244\234\347\264\242\343\201\247\343\201\256\350\246\213\343\201\210\346\226\271.csv" "b/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\346\244\234\347\264\242\343\201\247\343\201\256\350\246\213\343\201\210\346\226\271.csv" deleted file mode 100644 index 6549c17..0000000 --- "a/docs/data/search_console/buzzbase.jp-Performance-on-Search-2026-03-14/\346\244\234\347\264\242\343\201\247\343\201\256\350\246\213\343\201\210\346\226\271.csv" +++ /dev/null @@ -1 +0,0 @@ -検索での見え方,クリック数,表示回数,CTR,掲載順位 \ No newline at end of file diff --git a/docs/strategy/ios-app-download-growth-integrated-202604.md b/docs/strategy/ios-app-download-growth-integrated-202604.md new file mode 100644 index 0000000..ebb3667 --- /dev/null +++ b/docs/strategy/ios-app-download-growth-integrated-202604.md @@ -0,0 +1,174 @@ +# iOSアプリDL数増加 統合戦略レポート + +分析日: 2026-04-02 +分析期間データ: 2026/2/23〜3/22 + +--- + +## エグゼクティブサマリー + +6つの専門エージェントの分析結果から、**全エージェントが共通して最優先と判断した施策**は以下の3つ: + +1. **Smart App Banner の全ページ設置**(工数: 30分、コスト: 0円) +2. **計算ツール結果直後にアプリCTAを追加**(工数: 半日〜1日) +3. **App Storeメタデータ(タイトル・キーワード・説明文)の最適化**(工数: 1.5時間) + +この3つだけで、追加コスト0円・合計工数4-5時間で、月間30〜80DLの基盤が整う。 + +--- + +## 1. 現状分析(全エージェント共通認識) + +### トラフィック +- 月間アクティブユーザー: 838人(96%が新規) +- 流入: Google organic 58%、Yahoo 22%、Direct 14% +- 計算ツール系がSEO流入の主力(OPS計算: 月10,877表示) + +### 最大の課題 +**「トラフィックはあるがアプリに繋がっていない」** +- 検索→クリックの断絶: 高表示クエリのCTRが低い(0.7〜2.8%) +- 訪問→回遊の断絶: OPS計算ツール直帰率55% +- 登録→DLの断絶: 月170人が登録しているがアプリDL導線がない + +### 市場環境 +- ターゲット(中高生野球人口): 約28〜32万人 +- 現在の浸透率: 約0.3%(成長余地は大きい) +- 競合: ベボレコ(4,323レビュー)、TeamHub、ヒットメーカー等 +- BUZZ BASEの独自優位: 「グループランキング比較」は競合に存在しない機能 + +--- + +## 2. 優先度S施策(即実行・コストほぼゼロ) + +### S-1. Smart App Banner設置(工数: 30分) +- `layout.tsx` に meta tag 1行追加で全ページに一括適用 +- iOS Safariユーザー全員にアプリ訴求 +- 月10,000+インプレッションの計算ツールページでCVR 1〜3% = 月100〜300のApp Store流入 + +### S-2. App Storeメタデータ最適化(工数: 1.5時間) +- タイトル: 「BUZZ BASE - 野球成績ランキング」 +- サブタイトル: 「個人成績を記録・比較・共有しよう」 +- キーワード: `野球,成績,個人成績,打率,OPS,ランキング,野球部,記録,高校野球,中学野球,チーム,比較,打撃成績,投手成績` +- 競合が使っていない「ランキング」「成績比較」「中学野球」が独占可能な空白キーワード + +### S-3. 計算ツール結果直後にアプリCTA追加(工数: 半日〜1日) +- 計算完了後が最もモチベーションが高い瞬間 +- 結果値を動的に使った文言: 「OPS {value} を記録しよう。アプリなら毎試合の推移をグラフで確認できます」 +- 既存の `CtaBanner` に `resultValue` props を追加するだけで対応可能 + +--- + +## 3. 優先度A施策(低コスト・中期効果) + +### A-1. 高表示・0クリッククエリへの対応 +- 「opsとは」967表示/0クリック、「防御率計算」268表示/0クリック等 +- タイトル・ディスクリプション修正で月間+79クリック見込み +- 期待追加登録: +10〜15人/月 + +### A-2. OPS計算ツールページの直帰率改善(55%→40%目標) +- 計算完了後の次アクション誘導が機能していない +- 月間追加残留ユーザー: +30人、追加登録: +10〜12人 + +### A-3. レビュー獲得施策 +- SKStoreReviewController を適切なタイミングで表示 + - 成績入力5回目 + - 初回ランキング確認時 + - グループ参加成功時 +- 既存Webユーザー800人へ「初期サポーターバッジ」特典付きレビュー依頼 +- 目標: 3ヶ月でレビュー25件 + +### A-4. X(Twitter)公式アカウント運用 +- 現状すでに月10人がX経由で流入(唯一のSNSチャネル) +- 「野球垢」文化の中心がX、テキスト中心で運用コスト最低 +- 週3〜5時間、成績Tips系コンテンツを投稿 + +### A-5. 成績テキストシェア機能(工数: 1日) +- Expo の `Share.share()` API で実装 +- LINE/Xに成績をシェア → バイラル拡散の基礎 + +--- + +## 4. 優先度B施策(中期・工数中) + +### B-1. グループ招待URL機能(Issue #197、工数: 5〜7日) +- LINEで招待URL送付 → アプリ登録 → 自動グループ参加 +- チーム単位での普及を促進(中高生は口コミが最強のチャネル) + +### B-2. 打率・長打率計算ツールの新規ページ追加 +- 打率計算: 2,166表示/CTR 0.8% → CTR 3.5%で+58クリック/月 +- 長打率計算: 1,513表示/CTR 0.7% → CTR 3.5%で+43クリック/月 + +### B-3. iOSアプリ AdMob導入 +- バナー(ダッシュボード下部)、インタースティシャル(試合記録保存後のみ) +- UXを損なわない配置を厳守(1セッション最大2回上限) + +--- + +## 5. 3ヶ月ロードマップ + +### 4月(基盤構築)- 目標DL: +50 +| 週 | 施策 | 工数 | +|----|------|------| +| 第1週 | Smart App Banner設置 | 30分 | +| 第1週 | App Storeメタデータ最適化 | 1.5時間 | +| 第1週 | 計算ツールCTA追加 | 半日〜1日 | +| 第2週 | 0クリッククエリのタイトル・ディスクリプション修正 | 2〜3時間 | +| 第3週 | 成績テキストシェア機能 | 1日 | +| 第4週 | レビュー依頼ダイアログ実装 | 半日 | + +### 5月(導線最適化)- 目標DL: +80 +- OPS計算ツール直帰率改善 +- 打率・長打率計算ツールページ新設 +- X公式アカウント本格運用開始 +- 会員登録ページCVR改善(54%→65%目標) + +### 6月(拡大・検証)- 目標DL: +120 +- グループ招待URL機能リリース +- AdMob導入 +- Instagram運用開始 +- 夏の大会シーズンに向けたコンテンツ準備 + +--- + +## 6. KPI目標 + +| 指標 | 現状 | 1ヶ月後 | 3ヶ月後 | 6ヶ月後 | +|------|------|---------|---------|---------| +| 月間アプリDL | 推定34 | 50 | 120 | 250 | +| App Storeレビュー数 | 0 | 5 | 25 | 50 | +| Web月間セッション | 1,358 | 1,500 | 1,700 | 2,200 | +| 平均CTR | 1.5% | 2.5% | 3.5% | 4.0% | +| OPS計算ツール直帰率 | 55% | 50% | 40% | 35% | + +--- + +## 7. 競合との差別化ポイント + +| 競合 | レビュー数 | BUZZ BASEの優位性 | +|------|-----------|------------------| +| ベボレコ | 4,323 | グループランキング比較(ベボレコにない) | +| TeamHub | 4,377 | 無料・個人利用に特化(TeamHubは月額¥2,000〜) | +| ヒットメーカー | 1,809 | 他ユーザーとの成績比較機能(ヒットメーカーで要望多数) | + +**App Storeの説明文・スクリーンショットで「ランキング」を最前面に出すこと。** + +--- + +## 8. 収益シミュレーション + +| 時期 | 条件 | 月間収益 | +|------|------|---------| +| 現在 | WebのみRPM 108円 | 約454円 | +| 3ヶ月後 | アプリDAU 25 + Web RPM改善 | 約1,500円 | +| 6ヶ月後 | アプリDAU 75 + PV 10,000 | 約6,250円 | +| 12ヶ月後 | アプリDAU 250 + PV 30,000 + 課金 | 約21,800円 | + +--- + +## 関連ドキュメント + +- [全体戦略](ios-app-growth-strategy-202604.md) +- [ASO・マーケティング施策](marketing/aso-and-app-growth-plan.md) +- [競合分析](research/ios-growth-strategy-202604.md) +- [プロダクト企画](product/ios-app-download-growth.md) +- [収益化戦略](monetization-strategy-ios-web-202604.md) diff --git a/docs/strategy/ios-app-growth-strategy-202604.md b/docs/strategy/ios-app-growth-strategy-202604.md new file mode 100644 index 0000000..42a1aac --- /dev/null +++ b/docs/strategy/ios-app-growth-strategy-202604.md @@ -0,0 +1,683 @@ +# BUZZ BASE iOSアプリ DL数増加 & ASO戦略(2026年4月〜6月) + +作成日: 2026-04-02 +データ期間: 2026/2/23 - 3/22(Google Analytics / Search Console) + +--- + +## 目次 + +1. [現状診断と機会分析](#1-現状診断と機会分析) +2. [3ヶ月ロードマップ](#2-3ヶ月ロードマップ月別マイルストーン) +3. [ASO戦略](#3-asoapp-store-optimization戦略) +4. [Web→アプリ導線設計](#4-webアプリ導線設計) +5. [KPI設計](#5-kpi設計) +6. [施策の優先順位マトリクス](#6-施策の優先順位マトリクス効果-x-工数) +7. [リソース制約を踏まえた実行計画](#7-個人開発のリソース制約を踏まえた実行計画) + +--- + +## 1. 現状診断と機会分析 + +### iOSアプリを取り巻く環境 + +| 項目 | 状況 | +|------|------| +| iOSアプリ | リリース済み(時期不明、レビュー数・DL数が少ない段階と推定) | +| Webサイト月間訪問者 | 838人(うち新規808人) | +| SEO流入 | 月600クリック、25,000表示 | +| 主要検索クエリ | 「ops 計算」「野球 個人成績 アプリ 無料」「出塁率 計算」「打率計算」 | +| 都市分布 | 大阪74、神戸34、札幌34、横浜34、福岡33(全国分散型) | +| 競合最大手 | ヒットメーカー(45,000DL、評価4.8)、ベボレコ(推定2万DL、評価4.8) | +| BUZZ BASEの差別化 | ランキング機能(競合ゼロ)、チーム横断グループ、29種の成績自動計算 | + +### 最大の成長レバー + +**既にWebで月838人が訪問している。このトラフィックをアプリDLに転換することが最もROIが高い。** + +- 「野球 個人成績 アプリ 無料」で月45クリック(アプリを探しているユーザーが既に来ている) +- 計算ツール経由の月600クリックは、潜在的なアプリユーザー +- 新規登録ページに月170人が到達(高いインテント) + +### 機会の優先順位 + +| 機会 | インパクト | 根拠 | +|------|-----------|------| +| Web→アプリ導線の構築 | 最大 | 月838人の既存トラフィックを転換。コスト0円 | +| ASOキーワード最適化 | 大 | App Store内検索は競合が手薄な領域あり | +| 計算ツールからのアプリ誘導 | 大 | 月600SEOクリックの受け皿 | +| SNSでのアプリ訴求 | 中 | 現在のSNS流入は1.3%だが、成長余地あり | +| App Store内広告 | 小 | MAU規模では費用対効果が合わない | + +--- + +## 2. 3ヶ月ロードマップ(月別マイルストーン) + +### 全体方針 + +``` +4月(基盤構築) 5月(導線最適化) 6月(拡大・検証) +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ ASO基本設定 │ │ Web→アプリ導線│ │ 施策効果検証 │ +│ Web→アプリ │ │ A/B改善 │ │ 夏の大会準備 │ +│ バナー設置 │ │ レビュー促進 │ │ SNS連動強化 │ +│ スマートバナー │ │ SNS運用開始 │ │ 7-8月攻勢準備 │ +└─────────────┘ └─────────────┘ └─────────────┘ + DL目標: +50 DL目標: +80 DL目標: +120 + 累計: +50 累計: +130 累計: +250 +``` + +### Month 1: 4月 -- 基盤構築(新入部員シーズン) + +**テーマ**: 最小工数で最大の基盤を整備する + +| # | 施策 | 工数 | 期待効果 | 優先度 | +|---|------|------|---------|--------| +| 1 | App Storeメタデータ全面改訂(後述ASO戦略参照) | 2-3日 | 検索露出+40%、CVR向上 | P0 | +| 2 | Webサイト全ページにスマートバナー(apple-itunes-app meta tag)設置 | 2時間 | iOSユーザーの自然なDL誘導 | P0 | +| 3 | 計算ツール結果画面に「アプリで成績を継続記録」CTA追加 | 半日 | 月600SEOクリックからの転換 | P0 | +| 4 | 新規登録完了画面に「アプリをダウンロード」導線追加 | 半日 | 月170人の登録フローからの転換 | P0 | +| 5 | App Store用スクリーンショット5枚の刷新 | 1-2日 | CVR(閲覧→DL)の向上 | P1 | +| 6 | App Store用プレビュー動画(30秒)の制作 | 1-2日 | CVR向上+差別化訴求 | P2 | + +**月末マイルストーン**: ASO基本設定完了、Web全ページにアプリ導線設置済み、DL +50 + +### Month 2: 5月 -- 導線最適化・レビュー獲得(春季大会シーズン) + +**テーマ**: 転換率の改善とApp Store評価の強化 + +| # | 施策 | 工数 | 期待効果 | 優先度 | +|---|------|------|---------|--------| +| 7 | アプリ内レビュー促進の実装(成績入力5回目にダイアログ表示) | 半日 | レビュー数増加→ストア順位向上 | P0 | +| 8 | 計算ツールCTAの文言A/Bテスト(手動切替で検証) | 半日 | CTA→DLの転換率最適化 | P1 | +| 9 | 「OPSとは」等の解説ページにアプリDL導線を埋め込み | 半日 | 新設コンテンツからの誘導 | P1 | +| 10 | X/Instagram公式アカウントでアプリ紹介投稿(週1-2回) | 継続 | SNS経由のDL獲得 | P1 | +| 11 | App Storeの「What's New」を定期更新(2週間ごと) | 15分/回 | ストアアルゴリズムへの更新シグナル | P1 | +| 12 | 大会シーズン向けキーワード追加(「春季大会」「高校野球」関連) | 1時間 | 季節性の検索需要取り込み | P2 | + +**月末マイルストーン**: レビュー10件以上、CTA最適化完了、DL +80(累計+130) + +### Month 3: 6月 -- 拡大・夏の大会準備 + +**テーマ**: 7-8月の最大需要期に向けた仕込み + +| # | 施策 | 工数 | 期待効果 | 優先度 | +|---|------|------|---------|--------| +| 13 | App Store用スクリーンショットに「ランキング機能」を最前面に配置(検証結果反映) | 1日 | CVRの継続改善 | P0 | +| 14 | 成績カード画像のSNSシェア機能(OGP)にApp Storeリンクを含める | 1日 | シェア経由のDL獲得 | P1 | +| 15 | 計算ツールの新規追加(防御率計算強化、得点圏打率等)にアプリ導線を標準装備 | SEO施策と連動 | 新コンテンツからの自動誘導 | P1 | +| 16 | App Storeキーワードの見直し(1ヶ月の検索データ分析後) | 1時間 | 検索露出の最適化 | P1 | +| 17 | 「夏の大会で成績を記録しよう」キャンペーン準備 | 1日 | 7月のDL急増の種まき | P2 | +| 18 | ユニバーサルリンク/ディープリンクの整備(Web→アプリの直接遷移) | 1-2日 | 既存ユーザーのアプリ移行促進 | P2 | + +**月末マイルストーン**: レビュー20件以上、夏の大会キャンペーン準備完了、DL +120(累計+250) + +--- + +## 3. ASO(App Store Optimization)戦略 + +### 3-1. キーワード戦略 + +App Storeの検索アルゴリズムでは、以下の要素がランキングに影響する: +- **アプリ名**(最も影響大) +- **サブタイトル**(影響大) +- **キーワードフィールド**(100文字制限) +- **DL数・レビュー評価・アップデート頻度** + +#### アプリ名の最適化 + +``` +現状(推定): BUZZ BASE +改善案: BUZZ BASE - 野球成績ランキング +``` + +アプリ名に「野球」「成績」「ランキング」の3語を含めることで、App Store検索での露出を大幅に向上させる。30文字制限内で最大限のキーワードを含める。 + +#### サブタイトルの最適化 + +``` +改善案: 打率・防御率・OPSを自動計算して仲間と比較 +``` + +30文字制限内で以下を伝える: +- 具体的な指標名(検索キーワード) +- 「自動計算」(機能訴求) +- 「仲間と比較」(差別化訴求) + +#### キーワードフィールド(100文字、カンマ区切り) + +``` +個人成績,打率計算,出塁率,長打率,OPS,WHIP,防御率,野球部,高校野球,中学野球,スコア, +セイバーメトリクス,野球ノート,成績管理,チーム,グループ,無料,記録,投手,打撃 +``` + +**キーワード選定の根拠**: + +| キーワード | 選定理由 | +|-----------|---------| +| 個人成績, 成績管理 | Web検索で「野球 個人成績 アプリ 無料」が月45クリック。直接的なインテント | +| 打率計算, 出塁率, OPS | 計算ツールSEOで実証済みの高需要キーワード | +| 高校野球, 中学野球, 野球部 | ターゲット層が使う検索語 | +| セイバーメトリクス | 競合も使用。データ志向ユーザーの取り込み | +| 無料 | 中高生の検索行動で必ず付加される語 | +| チーム, グループ | BUZZ BASE独自のグループ機能を訴求 | + +#### キーワードフィールドの最適化ルール + +- アプリ名・サブタイトルに含まれる語はキーワードフィールドから除外(重複は無意味) +- 単数形のみ使用(App Storeは自動で複数形も検索対象にする) +- スペースはカンマで代替(1文字も無駄にしない) +- 助詞(の、を、で)は不要 + +### 3-2. ビジュアル戦略 + +#### スクリーンショット(5枚構成) + +App Storeでは最初の3枚が検索結果に表示される。この3枚で「インストールする理由」を伝え切る。 + +| 順番 | 内容 | キャプション | 狙い | +|------|------|------------|------| +| 1枚目 | ランキング画面 | 「仲間と成績をランキングで競い合おう」 | 最大の差別化ポイントを最前面に | +| 2枚目 | 成績サマリー画面 | 「29種の成績指標を自動計算」 | 機能の網羅性を訴求 | +| 3枚目 | グループ機能 | 「チームを超えてライバルとつながる」 | ソーシャル要素の訴求 | +| 4枚目 | 成績入力画面 | 「試合結果をサクッと記録」 | 使いやすさの訴求 | +| 5枚目 | 野球ノート画面 | 「野球ノートで振り返り。成長を実感」 | 付加価値の訴求 | + +**デザイン方針**: +- 背景色はBUZZ BASEのブランドカラーを使用 +- 端末モックアップ内にアプリ画面を配置 +- 大きなキャプションテキストを端末の上に配置(検索結果の小さいサムネイルでも読める) +- 中高生が共感するコピー(「ライバルに差をつけろ」等の競争心を刺激する表現) + +#### プレビュー動画(30秒) + +``` +0-5秒: フック「野球の成績、記録するだけで終わってない?」 +5-12秒: 成績入力のデモ(簡単さを訴求) +12-20秒: ランキング画面の表示(差別化ポイント) +20-25秒: グループ作成→友達の成績が表示される(ソーシャル要素) +25-30秒: CTA「今すぐ無料でダウンロード」 +``` + +### 3-3. 説明文の最適化 + +App Storeの説明文は検索ランキングに直接影響しないが、CVR(閲覧→DL転換率)に影響する。 + +#### プロモーションテキスト(170文字、審査なしで随時変更可能) + +``` +野球の個人成績をランキングで競い合える唯一のアプリ。打率・防御率・OPSなど +29種の指標を自動計算。チームメイトやライバルとグループを作って、成績をランキングで比較しよう。 +新入部員も3年生も、今すぐ始められる。完全無料。 +``` + +**4月版(新入部員シーズン)**: +``` +新学期、新しいチームで成績を競い合おう。BUZZ BASEなら試合結果を入力するだけで +打率・OPS・防御率など29種の成績を自動計算。チームメイトとランキングで比較できる。完全無料。 +``` + +**7月版(夏の大会シーズン)**: +``` +夏の大会、あなたの成績をランキングに残そう。試合結果を入力→打率・OPS・防御率を自動計算→ +チームメイトとランキングで比較。29種の成績指標を記録できるのはBUZZ BASEだけ。完全無料。 +``` + +#### 説明文(4,000文字)の構成 + +``` +[1段落目: フック+差別化] +BUZZ BASEは、野球の個人成績をランキング形式で仲間と共有・比較できる唯一のアプリです。 + +[2段落目: 課題提起] +「自分の打率、チームで何番目だろう?」 +「他のチームのピッチャーと防御率を比べてみたい」 +そんな想いに応えるために、野球経験16年の開発者が作りました。 + +[3段落目: 主要機能] +--- 主な機能 --- + +[ランキング機能] +- グループ内で打率・防御率・OPSなどをランキング表示 +- チームが違う友達ともグループを作れる +- 毎試合の結果がランキングに即反映 + +[成績自動計算(29種類)] +- 打撃: 打率、出塁率、長打率、OPS、ISO、BB/Kなど +- 投手: 防御率、WHIP、K/9、K/BB、FIPなど +- 試合結果を入力するだけで全指標を自動算出 + +[グループ機能] +- チーム単位でも、チームを超えた仲間同士でもOK +- 招待リンクをLINEで送るだけで参加できる + +[野球ノート] +- 試合の振り返りや練習メモを記録 +- 成績データと合わせて成長を実感 + +[4段落目: ターゲット訴求] +--- こんな人におすすめ --- +- チームメイトと成績を比べて刺激を受けたい中学生・高校生 +- 他チームのライバルと成績で勝負したい選手 +- 子どもの成績を記録・管理したい保護者 +- セイバーメトリクスに興味がある野球好き + +[5段落目: 安心感] +- 完全無料で全機能が使える +- 会員登録はメールアドレスまたはGoogleアカウントで簡単 +- 個人情報の取り扱いは厳重に管理 + +[6段落目: 開発者メッセージ] +BUZZ BASEは、16年間野球を続けてきた開発者が「こんなアプリが欲しかった」という +想いから作ったサービスです。ご意見・ご要望はアプリ内からお気軽にお寄せください。 +``` + +### 3-4. カテゴリ・その他設定 + +| 設定項目 | 推奨値 | 理由 | +|---------|-------|------| +| プライマリカテゴリ | スポーツ | ターゲットカテゴリ内での競争が「ソーシャル」より低い | +| セカンダリカテゴリ | ソーシャルネットワーキング | ランキング・グループ機能のソーシャル性を訴求 | +| 年齢レーティング | 4+ | 制限なしで最大リーチ | +| 対応言語 | 日本語(プライマリ) | 当面は日本市場に集中 | + +### 3-5. App Store内検索広告(Apple Search Ads) + +**現段階では実施しない。** 理由: +- MAU 70-90の段階では広告費用対効果が合わない +- まずオーガニックASO + Web→アプリ導線で基盤を作る +- MAU 500超、月間DL 100超になった段階で、競合名キーワード入札を検討 + +将来的な検討候補(月間DL 100超達成後): + +| キーワード | 入札理由 | 想定CPA | +|-----------|---------|---------| +| 「野球 成績 アプリ」 | 直接的なインテント | 200-400円 | +| 「ヒットメーカー」 | 競合ユーザーの奪取 | 300-500円 | +| 「野球 ランキング」 | 差別化ポイントと一致 | 150-300円 | + +--- + +## 4. Web→アプリ導線設計 + +### 設計原則 + +1. **ユーザーの意図に沿った自然な導線**(押し付けない) +2. **最もインテントが高い瞬間にCTAを表示** +3. **Webの体験を損なわない**(広告と同じ扱いにしない) + +### 4-1. スマートバナー(全ページ共通) + +Safariの標準スマートバナー機能を利用。HTMLの`<head>`に1行追加するだけ。 + +```html +<meta name="apple-itunes-app" content="app-id=YOUR_APP_ID"> +``` + +**効果**: iOSのSafariでWebサイトを訪れた全ユーザーに、画面上部にアプリバナーが表示される。ユーザーが閉じれば再表示されない。UXへの影響が最小限。 + +**工数**: 実装2時間(Next.jsのlayout.tsxにmeta tag追加) + +### 4-2. 計算ツール結果画面のCTA + +計算ツールは月600クリック(SEO流入の大半)を受ける最重要ページ。計算結果表示後が最もインテントが高い瞬間。 + +``` +[計算結果表示] + ↓ +[次のステップ提案] + 「この成績を継続的に記録しませんか?」 + ├── 「Webで成績を記録する →」(会員登録へ) + └── 「アプリで手軽に記録する →」(App Storeへ) + └── App Storeアイコン + 星評価 + 「無料」バッジ +``` + +**文言のカスタマイズ(ツール別)**: + +| ツール | CTA文言 | +|--------|---------| +| OPS計算 | 「今日の試合のOPSを記録して、チーム内ランキングを確認しよう」 | +| 打率計算 | 「打率.300を目指して毎試合記録。アプリなら10秒で入力完了」 | +| 防御率計算 | 「防御率の推移を記録。ピッチャー仲間とランキングで比較」 | +| 出塁率計算 | 「出塁率を含む29種の成績をアプリで自動管理」 | +| K/BB計算 | 「K/BBを含む全投手指標をアプリで一括管理」 | + +### 4-3. 新規登録フローへの埋め込み + +月170人が新規登録ページに到達している。この高インテントユーザーにアプリを訴求する。 + +``` +[新規登録完了画面] + ↓ + 「登録完了!次は成績を記録してみましょう」 + ├── 「Webで始める →」 + └── 「アプリで始める →」(App Storeバッジ) + 「アプリなら通知でランキング変動をお知らせ。 + いつでもサクッと成績入力できます。」 +``` + +### 4-4. 成績計算方法解説ページ(/calculation-of-grades) + +月10,877表示/166クリックの高トラフィックページ。ページ下部にアプリ訴求セクションを追加。 + +``` +[既存コンテンツ: 29指標の計算方法解説] + ↓ +[新規追加セクション] + 「29種の成績を自動計算できるアプリ」 + - App Storeアイコン + - 「試合結果を入力するだけ。面倒な計算は全部おまかせ。」 + - [App Storeからダウンロード] ボタン +``` + +### 4-5. ユニバーサルリンク/ディープリンク(6月以降) + +既存のWebユーザーがアプリをインストール済みの場合、Web URLからアプリの対応画面に直接遷移させる。 + +| WebのURL | アプリの遷移先 | +|----------|--------------| +| /dashboard | ダッシュボードタブ | +| /game-result/* | 成績記録画面 | +| /groups/* | グループ詳細画面 | +| /mypage/* | プロフィール画面 | + +**工数**: 1-2日(Apple App Site Association + Expo Routerの設定) +**優先度**: P2(基盤施策の効果を確認してから実施) + +### 4-6. 導線全体図 + +``` +[Google/Yahoo検索] + │ + ├── 計算ツール → 計算結果 → 「アプリで成績管理」CTA → App Store + │ └── 「Webで登録」→ 登録完了 → 「アプリもどうぞ」→ App Store + │ + ├── 解説ページ → 「自動計算アプリ」セクション → App Store + │ + └── 「野球 個人成績 アプリ」→ LP or 記事 → App Storeバッジ → App Store + ↑ +[SNS (Instagram/X)] │ + └── プロフィールリンク ──────────────────┘ + +[App Store内検索] + └── ASO最適化済みのメタデータ → アプリページ → DL + +[全ページ共通] + └── スマートバナー(iOS Safari) → App Store +``` + +--- + +## 5. KPI設計 + +### 5-1. 最重要KPI(North Star Metric) + +**iOSアプリの月間新規DL数** + +これを分解すると: + +``` +月間DL数 = App Store閲覧数 x CVR(閲覧→DL) + + Web経由のApp Storeタップ数 x CVR + + SNS経由のApp Storeタップ数 x CVR +``` + +### 5-2. KPIツリー + +``` +月間DL数(目標: 1ヶ月目50, 2ヶ月目80, 3ヶ月目120) +├── [App Store内] App Store検索経由DL +│ ├── インプレッション数(表示回数) +│ ├── プロダクトページ閲覧数 +│ └── CVR(閲覧→DL) +├── [Web経由] Web→App Store遷移→DL +│ ├── Webサイト訪問者数(現状838/月) +│ ├── App Store遷移率(CTAクリック率) +│ └── App Store CVR +└── [SNS経由] SNS→App Store→DL + ├── SNSフォロワー数 + ├── 投稿のリンククリック率 + └── App Store CVR +``` + +### 5-3. 追跡すべき指標と目標値 + +#### 主要KPI(週次追跡) + +| KPI | 現状(推定) | 1ヶ月後 | 2ヶ月後 | 3ヶ月後 | +|-----|------------|---------|---------|---------| +| 月間DL数 | - | 50 | 80 | 120 | +| App Storeインプレッション数 | - | 2,000 | 3,500 | 5,000 | +| App Storeプロダクトページ閲覧数 | - | 500 | 900 | 1,500 | +| App Store CVR(閲覧→DL) | - | 10% | 9% | 8% | +| Web→App Storeクリック数 | 0 | 150 | 300 | 500 | +| アプリ内レビュー数(累計) | - | 5 | 15 | 25 | +| アプリ内レビュー平均評価 | - | 4.5以上 | 4.5以上 | 4.5以上 | + +#### ドライバーKPI(月次追跡) + +| KPI | 現状 | 3ヶ月後目標 | 計測方法 | +|-----|------|-----------|---------| +| Webサイト月間訪問者 | 838 | 1,500 | GA4 | +| 計算ツールCTAクリック率 | 0% | 3% | GA4イベント | +| スマートバナー表示→DL転換率 | 0% | 1% | App Store Connect | +| App Store検索キーワード順位(「野球 成績」) | 圏外 | 10位以内 | App Store Connect | +| App Store検索キーワード順位(「野球 ランキング」) | 圏外 | 5位以内 | App Store Connect | +| アプリ7日リテンション率 | 未計測 | 30% | App Store Connect / Firebase | + +#### 先行指標(施策の早期効果検証用) + +| 指標 | 確認頻度 | 意味 | +|------|---------|------| +| App Storeインプレッション数の前週比 | 週次 | ASOの効果が出ているか | +| Web→App Storeクリック数 | 週次 | 導線CTAが機能しているか | +| CVR(閲覧→DL) | 週次 | スクリーンショット・説明文の訴求力 | +| レビュー投稿数 | 週次 | レビュー促進施策の効果 | + +### 5-4. 計測方法 + +| データソース | 取得できる指標 | +|------------|--------------| +| App Store Connect - App Analytics | インプレッション、プロダクトページ閲覧数、DL数、CVR、ソース別内訳 | +| App Store Connect - 検索広告 | キーワード別インプレッション・タップ数(Search Ads利用時) | +| GA4(Webサイト) | CTA クリック数(イベント計測)、App Storeリンクのクリック | +| Firebase Analytics(アプリ内) | リテンション率、DAU/MAU、画面別利用率 | +| Search Console | Web検索のインプレッション・クリック・順位 | + +### 5-5. 週次レビューテンプレート + +``` +## iOSアプリ 週次レビュー(日付) + +### DL数 +- 今週のDL数: ___(前週比 ___%) +- 月初からの累計: ___(月間目標の___%) + +### App Store +- インプレッション: ___(前週比 ___%) +- プロダクトページ閲覧: ___ +- CVR: ___% +- 新規レビュー: ___件(累計___件、平均___点) + +### Web→アプリ導線 +- CTA表示回数: ___ +- CTAクリック数: ___ +- クリック率: ___% + +### 今週の施策と結果 +- ___ + +### 来週の予定 +- ___ +``` + +--- + +## 6. 施策の優先順位マトリクス(効果 x 工数) + +### マトリクス図 + +``` +効果(大) +│ +│ [A] スマートバナー [B] ASO全面改訂 +│ 設置 スクリーンショット刷新 +│ (2時間) (2-3日) +│ +│ [C] 計算ツールCTA [D] レビュー促進 +│ 追加 実装 +│ (半日) (半日) +│ +│ [E] 登録完了画面 [F] プレビュー動画 +│ アプリ導線 制作 +│ (半日) (1-2日) +│ +│────────────────────────────────────────── +│ +│ [G] SNSでアプリ [H] ユニバーサルリンク +│ 紹介投稿 整備 +│ (継続/低) (1-2日) +│ +│ [I] 成績カード [J] Apple Search Ads +│ シェアにリンク (MAU500後) +│ (1日) (予算必要) +│ +効果(小) + 工数(小)────────────────工数(大) +``` + +### 優先順位リスト + +| 順位 | 施策 | 効果 | 工数 | ROI | 実施時期 | +|------|------|------|------|-----|---------| +| 1 | スマートバナー設置 | 大 | 極小(2時間) | 極高 | 4月第1週 | +| 2 | ASO基本設定(名前・サブタイトル・キーワード) | 大 | 小(半日) | 極高 | 4月第1週 | +| 3 | 計算ツール結果画面CTA追加 | 大 | 小(半日) | 極高 | 4月第1-2週 | +| 4 | 新規登録完了画面のアプリ導線 | 大 | 小(半日) | 高 | 4月第2週 | +| 5 | アプリ内レビュー促進の実装 | 大 | 小(半日) | 高 | 4月第2-3週 | +| 6 | スクリーンショット5枚の刷新 | 大 | 中(1-2日) | 高 | 4月第2-3週 | +| 7 | App Store説明文の最適化 | 中 | 小(半日) | 高 | 4月第3週 | +| 8 | 解説ページへのアプリ導線埋め込み | 中 | 小(半日) | 中 | 5月第1週 | +| 9 | プレビュー動画制作 | 中 | 中(1-2日) | 中 | 5月中 | +| 10 | 成績カードシェアにApp Storeリンク | 中 | 小(1日) | 中 | 6月 | +| 11 | ユニバーサルリンク整備 | 中 | 中(1-2日) | 中 | 6月 | +| 12 | SNSでのアプリ紹介投稿 | 小-中 | 継続 | 中 | 5月〜 | + +--- + +## 7. 個人開発のリソース制約を踏まえた実行計画 + +### 前提条件 + +- 開発者1名(フルタイムではない) +- 平日の開発可能時間: 推定2-3時間/日 +- 週末: 追加で数時間 +- 並行タスク: Webサイト開発、SEO施策、SNS運用開始、広告最適化 +- 予算: 最小限(月1万円以下) + +### 週次実行計画 + +#### 4月第1週(4/1-4/7)-- 最重要の3施策を完了 + +| 日 | タスク | 工数 | +|----|--------|------| +| 水 | スマートバナー設置(meta tag追加のみ) | 30分 | +| 水 | App Store: アプリ名・サブタイトル・キーワード更新 | 1時間 | +| 木 | App Store: プロモーションテキスト更新 | 30分 | +| 木 | App Store: 説明文の更新 | 1時間 | +| 金-土 | 計算ツール結果画面にアプリCTA追加 | 2-3時間 | + +**この週だけで最もROIの高い施策の80%が完了する。** + +#### 4月第2週(4/8-4/14) + +| タスク | 工数 | +|--------|------| +| 新規登録完了画面にアプリ導線追加 | 2時間 | +| アプリ内レビュー促進ダイアログの実装 | 2-3時間 | +| GA4にCTAクリックのイベント計測を追加 | 1時間 | + +#### 4月第3-4週(4/15-4/30) + +| タスク | 工数 | +|--------|------| +| スクリーンショット5枚の制作・更新 | 4-5時間 | +| App Store Connect で初期データ確認 | 30分 | +| 第1週施策の効果測定(インプレッション・CTAクリック数) | 30分 | + +#### 5月(CTA最適化 + レビュー獲得に集中) + +| 週 | タスク | 工数/週 | +|----|--------|--------| +| 第1週 | 解説ページにアプリ導線セクション追加 | 2時間 | +| 第2週 | CTA文言の改善(4月のデータを基に) | 1時間 | +| 第3週 | SNS(X/Instagram)でアプリ紹介投稿開始 | 継続(週1-2時間) | +| 第4週 | App Storeキーワードの効果分析・微調整 | 1時間 | +| 毎週 | レビュー依頼への返信(App Store Connect) | 15分 | + +#### 6月(拡大施策 + 夏の大会準備) + +| 週 | タスク | 工数/週 | +|----|--------|--------| +| 第1週 | プレビュー動画の制作 | 3-4時間 | +| 第2週 | 成績カードシェアにApp Storeリンク含める | 2-3時間 | +| 第3週 | ユニバーサルリンクの設定 | 3-4時間 | +| 第4週 | 夏の大会キャンペーン用プロモーションテキスト更新 | 30分 | +| 毎週 | 週次KPIレビュー | 15分 | + +### やらないことリスト(3ヶ月間) + +| やらないこと | 理由 | +|-------------|------| +| Apple Search Ads(有料広告) | MAU規模では費用対効果が合わない | +| インフルエンサーマーケティング | 予算制約。まずオーガニック施策を証明する | +| Android版の同時最適化 | iOSに集中してPDCAを回す。Android版は効果実証後 | +| 複雑なA/Bテスト | DL数が少ない段階では統計的に有意にならない | +| App Storeの多言語対応 | 日本市場に集中 | +| アプリ独自機能の大型開発 | まず既存機能のDL転換を最大化する | + +### コスト + +| 項目 | 費用 | 備考 | +|------|------|------| +| Apple Developer Program | 12,980円/年(既に支払い済み) | - | +| スクリーンショット制作 | 0円 | Figma無料プラン + 自作 | +| プレビュー動画制作 | 0円 | CapCut等の無料ツール | +| **3ヶ月合計** | **0円**(追加費用なし) | | + +--- + +## 7月以降の展望(夏の大会シーズン) + +3ヶ月の基盤構築が成功すれば、7-8月は年間最大の需要期を迎える。 + +| 施策 | 時期 | 期待効果 | +|------|------|---------| +| 夏の大会連動プロモーションテキスト | 7月頭 | 季節性キーワードでの露出増 | +| 「大会成績を記録しよう」キャンペーン(SNS) | 7-8月 | 大会への関心を活用したDL促進 | +| 甲子園連動コンテンツ(Web→アプリ導線含む) | 8月 | 年間最高の野球関心を活用 | +| 新チーム始動キャンペーン(9月) | 9月 | 3年生引退後の新グループ作成需要 | + +**3ヶ月後の目標達成状況に応じた分岐**: + +| 達成度 | 7月以降のアクション | +|--------|-------------------| +| 目標の120%以上(DL 300+) | Apple Search Adsのテスト開始、SNS広告の小規模テスト | +| 目標の80-120%(DL 200-300) | 現施策の継続・改善。夏の大会連動で加速 | +| 目標の80%未満(DL 200以下) | 原因分析。ユーザーインタビュー実施。導線・訴求の根本見直し | + +--- + +## 付属ドキュメント + +| ドキュメント | パス | +|-------------|------| +| 競合アプリ調査レポート | `docs/strategy/research/competitor-analysis-app-store-202503.md` | +| グロース&マーケティング施策書 | `docs/strategy/marketing/growth-plan.md` | +| 収益化プロダクトロードマップ | `docs/strategy/product/product-roadmap.md` | +| SEO & 広告収益成長戦略 | `docs/strategy/seo-growth-plan-202604.md` | +| PDCA第1期戦略計画書 | `docs/strategy/pdca/cycle1-plan.md` | + +--- + +*本戦略書は2026年4月2日時点の分析に基づく。月次でKPIを確認し、効果が出ていない施策は2週間で見直す。* diff --git a/docs/strategy/marketing/aso-and-app-growth-plan.md b/docs/strategy/marketing/aso-and-app-growth-plan.md new file mode 100644 index 0000000..9399a44 --- /dev/null +++ b/docs/strategy/marketing/aso-and-app-growth-plan.md @@ -0,0 +1,562 @@ +# BUZZ BASE iOS ダウンロード数増加・ASO施策書 + +**作成日**: 2026年4月2日 +**対象**: iOSアプリ(App Store) +**前提データ**: 2026年2月23日〜3月22日(Web) + +--- + +## 現状サマリーと課題 + +| 指標 | 数値 | +|------|------| +| Web MAU | 838人(新規808人)| +| 主要流入 | Google Organic 58% / Yahoo 22% / Direct 14% | +| SNS流入 | X 10人 / Instagram 1人(ほぼゼロ) | +| OPS計算ツール表示回数 | 10,877回/月 | +| 主要検索クエリ1位 | 「ops 計算」72クリック | +| 主要検索クエリ2位 | 「野球 個人成績 アプリ 無料」45クリック | + +**重要な気づき**: 「野球 個人成績 アプリ 無料」で月45クリック獲得しているということは、アプリを探しているユーザーが既にWebに流入している。このユーザーをApp Storeへ誘導する導線が機能していれば、リリース直後から一定のDL数が見込める。 + +--- + +## 1. ASO(App Store Optimization)具体策 + +### 1-1. キーワード戦略 + +#### 狙うべきキーワードの分類 + +**軸1: 検索ボリュームが大きく競合が少ない「野球×データ管理」系** + +野球アプリ市場は「チーム管理型」(TeamHub等)が中心で、「個人成績のランキング・共有」に特化したアプリは存在しない。この空白地帯のキーワードを優先して押さえる。 + +| キーワード | 狙い方 | 根拠 | +|-----------|--------|------| +| 野球 個人成績 | タイトル・サブタイトルに含める | Webで月45クリック獲得済み(実証済み需要)| +| 野球成績管理 | キーワードフィールドに設定 | 競合アプリが少ない空白キーワード | +| 野球ランキング | キーワードフィールドに設定 | BUZZ BASEの核心機能、競合ゼロ | +| 打率計算 | キーワードフィールドに設定 | Webで表示1,022回、アプリでも需要あり | +| OPS計算 | キーワードフィールドに設定 | Webで月10,877表示の最大流入キーワード | + +**軸2: 汎用的だが登録必須の「野球アプリ」系** + +| キーワード | 狙い方 | 注意点 | +|-----------|--------|--------| +| 野球 アプリ 無料 | キーワードフィールドに設定 | 競合多数。実績積み上げ後に上位を狙う | +| 野球 成績 | キーワードフィールドに設定 | 汎用的だが必ず登録する | +| 高校野球 成績 | キーワードフィールドに設定 | ターゲット層に直接刺さる | +| 中学野球 成績 | キーワードフィールドに設定 | ターゲット層に直接刺さる | + +**軸3: リテンション・口コミにつながる「ソーシャル」系** + +| キーワード | 狙い方 | +|-----------|--------| +| 野球 チーム 比較 | キーワードフィールドに設定 | +| 野球ノート | キーワードフィールドに設定 | +| 野球 記録 友達 | キーワードフィールドに設定 | + +#### App Storeキーワードフィールド(100文字以内)最適案 + +``` +野球,成績,個人成績,打率,OPS,ランキング,野球部,記録,高校野球,中学野球,チーム,比較,打撃成績,投手成績 +``` + +スペースなし・カンマ区切り・重複なし・App Storeタイトルに含む単語は除外する。 + + +### 1-2. タイトル・サブタイトル・説明文の最適化案 + +#### アプリ名(30文字以内) + +``` +BUZZ BASE - 野球成績ランキング +``` + +- 「BUZZ BASE」: ブランド認知のため先頭に固定 +- 「野球成績ランキング」: 検索にヒットする主要キーワードを3語含める +- App Storeではタイトルに含まれる単語が検索対象になるため、ここにキーワードを入れるのが最も効果的 + + +#### サブタイトル(30文字以内) + +``` +個人成績を記録・比較・共有しよう +``` + +- 「個人成績」「記録」「比較」「共有」をすべてカバー +- 動詞を並べることでアプリの用途が即座に伝わる +- サブタイトルも検索インデックスの対象 + + +#### 説明文(先頭3行が最重要 — 「続きを読む」の前に表示される部分) + +``` +野球部員のための個人成績管理アプリ。 +打率・OPS・防御率などを自動計算し、チームメイトとランキングで競い合えます。 +中学・高校の野球選手 10,000人以上が利用中。 + +【こんな人におすすめ】 +・打率や OPS など、複雑な成績指標を自動で計算したい +・チームメイトと成績を比較してモチベーションを上げたい +・試合ごとに個人成績を記録して成長を実感したい +・野球ノートをデジタルで管理したい + +【主な機能】 +■ 個人成績の自動計算 +打率・出塁率・長打率・OPS・防御率・WHIP など、主要な成績指標をすべて自動計算。難しい計算は不要。 + +■ ランキング機能 +グループ内でリアルタイムにランキングを確認。成績が数字で可視化されるので、ライバルと切磋琢磨できます。 + +■ グループ・チーム機能 +チームメイトをグループに招待して、成績を共有。学年別・ポジション別のランキングも確認できます。 + +■ 野球ノート +練習や試合後の振り返りをデジタルで記録。成績データと連動して成長を振り返れます。 + +■ 成績グラフ・分析 +シーズンを通じた成績の推移をグラフで確認。調子の波が一目でわかります。 + +【無料で使えます】 +基本機能はすべて無料。登録も1分で完了します。 + +開発者は野球経験16年のエンジニアです。選手目線の機能改善を継続的に行っています。 +要望・不具合は App Store レビューまたは公式サイトからお気軽にどうぞ。 +``` + +**説明文の設計意図**: +- 先頭3行でサービスの本質と規模感を伝える(続きを読まなくても判断できる) +- 「こんな人におすすめ」でターゲットが自分ごと化できる +- 機能一覧はキーワードを自然に含めながら書く(Google Playではランキング要因) +- 「無料」を明記してDLの心理的障壁を下げる +- 個人開発であることを強みに転換(開発者の顔が見える) + + +### 1-3. スクリーンショット・プレビュー動画戦略 + +#### スクリーンショットの並び順と設計方針 + +App Storeでは最初の2〜3枚が検索結果一覧に表示される。最初の1枚で「何のアプリか」が伝わらなければDLされない。 + +| 順番 | 画面 | キャプションテキスト | 目的 | +|------|------|-------------------|------| +| 1枚目 | ランキング画面 | 「チームメイトと成績で競え」 | 最大の差別化ポイントを最初に見せる | +| 2枚目 | 成績入力・自動計算画面 | 「打率もOPSも自動で計算」 | 主要機能訴求・計算ツール流入ユーザーへの訴求 | +| 3枚目 | グループ招待画面 | 「チーム全員で使えるグループ機能」 | ソーシャル要素訴求、招待による口コミを示唆 | +| 4枚目 | 成績グラフ画面 | 「シーズンを通じた成長が見える」 | データ蓄積の価値訴求、継続利用動機 | +| 5枚目 | 野球ノート画面 | 「練習の振り返りもデジタルで」 | 付加価値訴求 | + +**スクリーンショットのデザイン指針**: +- 背景色: BUZZ BASEのブランドカラー(紺・黒系)で統一感を出す +- キャプション: 白文字で大きく。2〜3語で完結する +- 実機フレーム: iPhoneのモックアップに入れる(Canva等で無料作成可能) +- 文字サイズ: App Storeの一覧表示でも読めるサイズに + + +#### プレビュー動画(30秒以内) + +プレビュー動画は検索結果一覧で自動再生される。最初の5秒で離脱を防ぐことが最重要。 + +**構成案(30秒)**: + +``` +0〜3秒: 「チームメイトと成績で競え」のキャッチコピー + ブランドロゴ +3〜12秒: 成績入力画面 → 自動計算される様子(OPS等の数値が出るところ) +12〜22秒: ランキング画面でユーザーが1位になるシーン → グループ内でのランキング +22〜28秒: グループ招待 → チームメイトの成績が並ぶ画面 +28〜30秒: 「無料ダウンロード」CTA + App Storeバッジ +``` + +**音声**: ナレーション不要。BGMにテンション高めの野球系BGMを薄くかける程度でOK。 +**制作ツール**: CapCut(無料)+ Xcode Simulatorの録画機能 + + +### 1-4. その他のASOメタ設定 + +| 項目 | 推奨設定 | 理由 | +|------|---------|------| +| プライマリカテゴリ | スポーツ | 検索ユーザーの属性に合致 | +| セカンダリカテゴリ | ソーシャルネットワーキング | ランキング・グループ機能が差別化ポイント | +| 年齢制限 | 4+ | 中高生がDLできるように制限なし | +| 価格 | 無料 | 中高生の可処分所得を考慮。IAP(アプリ内課金)で後々マネタイズ | +| 対応言語 | 日本語のみ | ターゲットが日本の中高生のみ | +| 更新頻度 | 2〜4週間に1回 | 更新頻度がストアの評価に影響する | + + +### 1-5. レビュー獲得施策 + +App Storeの検索順位はレビュー数・評価スコアが大きく影響する。リリース直後の最初の50件が最も重要。 + +#### アプリ内レビュー依頼(SKStoreReviewRequest) + +**依頼タイミングの設計**(ポジティブな体験直後に限定する): + +| トリガー | 条件 | 理由 | +|---------|------|------| +| 成績入力完了 | 累計5回目の入力完了時 | 「使えた」という実感が生まれた後 | +| ランキング確認 | 初めて自分のランキングを確認した後 | サービスの価値を体験した直後 | +| グループ参加 | チームメイトのグループに参加成功時 | ソーシャル要素を体験した直後 | + +**依頼しないタイミング(重要)**: +- アプリ起動直後 +- エラー発生後 +- 入力途中 + +#### Webユーザーへのレビュー依頼 + +既存のWebユーザー800人以上に対し、iOSアプリリリース時にサイト内通知でレビューを依頼する。 + +``` +通知文案: +「BUZZ BASE iOSアプリをリリースしました! +ぜひダウンロードして、App Storeでレビューを書いていただけると嬉しいです。 +レビューを書いてくれた方には[特典]をプレゼントします」 +``` + +特典案(コスト0): プロフィールに「初期サポーター」バッジを付与 + +#### SNSでのレビュー依頼 + +リリース後1〜2週間以内に、SNSのフォロワーに向けてレビュー依頼の投稿を行う。「1分で書けます」「開発者1人で作っています」という個人開発の訴求が中高生には刺さりやすい。 + +--- + +## 2. Web → App Storeへの導線強化策 + +### 2-1. Smart App Bannerの設置 + +HTMLの`<head>`に1行追加するだけで実装できる。iSafariでWebサイトを閲覧したユーザーに対し、画面上部にApp Storeへのバナーが自動表示される。 + +**設置対象ページ(優先度順)**: + +1. 計算ツール全ページ(`/tools/*`)— 月10,000以上の表示があり最大の流入源 +2. 成績計算ページ(`/calculation-of-grades`)— 月8,000以上の表示 +3. トップページ +4. 全ページ(meta tagはすべてのページに設置でもOK) + +**設置コード(Next.jsのlayout.tsxのmetadataに追加)**: + +```html +<meta name="apple-itunes-app" content="app-id=XXXXXXXXXX"> +``` + +※ app-idはApp Store Connect で取得したアプリIDを使用 + +**期待効果**: +- 月10,000インプレッションがあるOPS計算ツールから自動的にApp Storeへの導線ができる +- 追加実装コストがほぼゼロ +- CVR 1〜3%でも月100〜300のApp Storeページ流入が見込める + + +### 2-2. 計算ツールページからアプリへの誘導 + +計算ツールページはSEO流入の核だが、現状は「計算して終わり」になっている可能性が高い。ここにアプリへの誘導を組み込む。 + +#### パターンA: 計算結果の下にアプリ訴求バナーを表示 + +計算ボタンを押して結果が表示された直後に、以下のようなバナーを表示する。 + +``` +------------------------------------------- +この成績を毎試合記録したい方へ +BUZZ BASE アプリなら試合ごとに成績を記録して +シーズン全体の成績を自動で管理できます。 + +[App Storeでダウンロード(無料)] +------------------------------------------- +``` + +**設計のポイント**: +- 計算結果表示後のみ表示する(計算前に出すと邪魔) +- 「この成績を...」という表現で計算したばかりの成績と紐付ける +- 「無料」を明記 +- スマホユーザーには直接App Storeへ、PCユーザーにはQRコードを表示 + + +#### パターンB: 計算ツールページのCTA(Call to Action)最適化 + +現状のCTAが「会員登録」のみであれば、モバイルユーザーには「アプリでダウンロード」を優先表示する。 + +``` +PC表示: 「無料で始める(Web版)」 +スマホ表示: 「App Storeでダウンロード(無料)」を優先、その下に「Web版を使う」 +``` + +User-Agent判定で出し分けするか、両方のボタンを並べる。 + + +#### パターンC: 定期的な成績記録の訴求 + +OPS計算ツールを使ったユーザーに対して「毎回計算するのは面倒ではないですか?」という訴求をする。 + +``` +「OPSを毎回自分で計算していませんか? +BUZZ BASEアプリに安打数・打数などを入力するだけで +打率・OPS・出塁率が全部自動計算されます。」 +``` + + +### 2-3. その他の導線強化 + +| 施策 | 実装箇所 | 工数 | +|------|---------|------| +| フッターにApp Storeバッジを設置 | 全ページのフッター | 低 | +| トップページにアプリ訴求セクション追加 | LP | 低 | +| ログイン済みユーザーへのアプリ移行促進バナー | ダッシュボード | 低 | +| 「アプリでもっと便利に」モーダル(初回訪問時) | 全ページ | 中 | + +--- + +## 3. SNSマーケティング施策 + +### 3-1. 現状の課題分析 + +SNS流入は合計11人(X: 10人、Instagram: 1人)と、ほぼゼロの状態。ただし、現状はSNSアカウント未運用の可能性が高く、「やっていないから流入がない」という状態であれば改善余地は大きい。 + +**前提確認事項**: +- Instagram・X・TikTokの公式アカウントを開設しているか +- 開設済みであれば投稿頻度・内容の問題 +- 未開設であれば即時開設が必要 + + +### 3-2. 中高生野球部員へのリーチ戦略 + +中高生へのリーチでSNS広告(有料)は不要。以下の2点を押さえれば有機的に広がる。 + +**鉄則1: 「野球垢」文化に乗っかる** + +中高生の野球部員は「野球垢」(野球専用のSNSアカウント)を持つ文化がある。このコミュニティに入り込むには、広告的な投稿より「野球選手の日常あるある」「成績ネタ」のほうが圧倒的に刺さる。 + +**鉄則2: UGC(ユーザー生成コンテンツ)を活用する** + +「俺の成績をBUZZ BASEでランキング1位だった」という投稿をユーザーが自発的にしてくれる仕組みを作ることがゴール。アプリのスクリーンショットをシェアしやすい設計にすることが先決。 + + +### 3-3. プラットフォーム別コンテンツ戦略 + +#### X(Twitter)— 最優先・今すぐ開始 + +**なぜXを最優先にするか**: +- 野球部員の「野球垢」文化がXに集中している +- リプライ・引用RT・いいねで有機的に広がりやすい +- 運用コストが最も低い(テキスト中心) +- 現状10人の流入があるということはXで検索されている + +**投稿テーマと頻度**: + +| テーマ | 週あたりの投稿数 | 形式 | 狙い | +|-------|---------------|------|------| +| 野球選手あるある | 2〜3回 | テキスト+画像 | リーチ拡大・フォロワー獲得 | +| 成績Tips(「OPSが.800以上なら好打者」等) | 1〜2回 | 画像カード | 知識提供で保存・RTを促す | +| BUZZ BASEの使い方・機能紹介 | 1回 | 画像/動画 | ダウンロード促進 | +| 大会シーズン連動投稿 | 大会期間中に集中 | テキスト+画像 | 旬のコンテンツでリーチ拡大 | +| 開発日記・アップデート告知 | 月1〜2回 | テキスト | 個人開発の親近感、ファン獲得 | + +**具体的な投稿文案(すぐ使えるテンプレート)**: + +``` +【野球あるある系】 +「打率3割って言うと聞こえいいけど、 +実際は10打席で7回凡退してるっていう現実」 +#野球あるある #野球部 + +--- + +【成績Tips系】 +野球の成績指標、何を見ればいいか迷う人へ + +打者の総合評価なら「OPS」を見るのがおすすめ +出塁率 + 長打率 = OPS + +0.700未満 → 要改善 +0.700〜0.800 → 普通 +0.800以上 → 好打者 +1.000以上 → エリート打者 + +あなたのOPSはいくつ? +#野球 #野球部 #打率 #OPS + +--- + +【アプリ紹介系】 +チームメイトと打率で勝負したい野球部員へ + +BUZZ BASEなら成績を入力するだけで +・打率・OPS・出塁率を自動計算 +・チーム内ランキングをリアルタイム確認 +・「俺のほうが打ってる」が数字で証明できる + +App Storeで無料ダウンロード +[URL] +#野球 #野球部 #高校野球 +``` + + +#### Instagram — 次に優先 + +**特性**: ビジュアル重視、リール(短尺動画)が最もリーチが広い、ストーリーズは既存フォロワーへのリテンションに有効。 + +**投稿テーマと頻度**: 週2〜3回(リール1〜2本 + フィード1本) + +| テーマ | 形式 | 投稿頻度 | 制作コスト | +|-------|------|---------|----------| +| アプリの使い方デモ(画面録画) | リール30秒 | 週1 | 低(画面録画+CapCut) | +| 成績指標の解説(OPSとは、等) | カルーセル | 週1 | 低(Canvaで作成) | +| ユーザーの成績カードをリポスト | ストーリーズ | 週2〜3 | ほぼゼロ | +| 野球あるある | リール15秒 | 週1 | 低 | +| 大会シーズン連動 | フィード | 大会中のみ | 低 | + +**リールの具体的な構成案**: + +``` +パターン1: 「OPSの計算を自動化する動画」(30秒) +0〜5秒: 「OPSを毎回自分で計算してる人へ」テキスト +5〜20秒: BUZZ BASEに打数・安打数を入力 → OPSが自動計算される画面 +20〜28秒: ランキング画面でチームメイトと比較している様子 +28〜30秒: 「無料ダウンロードはプロフリンクから」 + +パターン2: 「成績で仲間と競い合う動画」(30秒) +0〜3秒: 「チームで誰が一番打ってる?」テキスト +3〜15秒: グループのランキング画面。1位〜5位が並んでいる +15〜25秒: 成績入力 → ランキングが更新される様子 +25〜30秒: 「チームメイトを招待して競おう」+ App Storeリンク +``` + + +#### TikTok — 余裕ができたら開始(Phase 3以降) + +TikTokはInstagram・Xが安定してから追加する。個人開発の工数を考慮すると、最初から3プラットフォーム同時運用は継続困難になるリスクが高い。 + +ただし、以下の条件が揃ったら優先度を上げる: +- Instagramのリールが週1本以上継続できている +- Instagram フォロワー 300人を達成 +- TikTokの投稿内容はInstagramのリールを転用可能(編集コスト低) + + +### 3-4. SNS運用を継続させるための仕組み + +個人開発でSNS運用が続かない最大の理由は「ネタ切れ」と「忙しさ」。以下のテンプレート化で対応する。 + +**投稿テンプレートの事前作成**: +- Canvaで成績カード・成績Tips画像のテンプレートを10枚作成 +- 数値だけ変えれば毎週投稿できる状態にする + +**バッファ管理**: +- 週1時間の投稿作成時間を確保し、2週間分まとめて作る +- Meta Business Suite(Instagram/Xの予約投稿)で自動投稿化 + +**コンテンツの分類**: +- 「常緑コンテンツ」: 成績Tips、野球あるあるは時期問わず使える → ストック作成 +- 「旬コンテンツ」: 大会連動投稿は大会2週前から計画 → カレンダーで管理 + +--- + +## 4. ユーザー獲得チャネル別の期待効果と優先度 + +### チャネル評価マトリクス + +| チャネル | 期待効果 | 実行コスト | 即効性 | 優先度 | +|---------|---------|----------|--------|--------| +| **Smart App Banner** | 月100〜300のApp Store流入(月10,000インプレのうち1〜3%) | ほぼゼロ(1行のmetaタグ) | 即日 | **S** | +| **計算ツールページのアプリCTA** | 月50〜200のApp Store流入 | 低(バナー1つ追加) | 即日〜1週間 | **S** | +| **SEO(計算ツール追加・既存改善)** | 月1,000〜2,000のWeb新規流入増 | 低(開発工数のみ) | 2〜4週間 | **A** | +| **X公式アカウント運用** | 月20〜50のApp Store流入(6ヶ月で) | 低(週3〜5時間) | 3〜6ヶ月 | **A** | +| **Instagram公式アカウント運用** | 月30〜100のApp Store流入(6ヶ月で) | 中(週3〜5時間 + 画像制作) | 3〜6ヶ月 | **A** | +| **アプリ内レビュー依頼** | レビュー数増 → 検索順位向上 | ほぼゼロ(実装のみ) | リリース直後 | **A** | +| **Webユーザーへのアプリ移行促進** | 既存800人のうち10〜30%がアプリへ | 低(バナー追加) | 1〜2週間 | **A** | +| **TikTok運用** | 月50〜200のApp Store流入(1年で) | 中(週3〜5時間 + 動画制作) | 6〜12ヶ月 | **B** | +| **プレスリリース(PR TIMES無料枠)** | アプリリリース時の認知拡大 | ほぼゼロ | リリース時点のみ | **B** | +| **YouTube** | 月10〜50のApp Store流入 | 高(動画制作コスト大) | 6〜12ヶ月 | **C** | + +**まとめ**: Smart App BannerとCTAの追加は「コストほぼゼロ×即効性あり×既存流入を活かせる」という観点で最優先。SEOはすでに最大の成長エンジンなので強化継続が鉄則。SNSは中長期の施策として位置づける。 + +--- + +## 5. 具体的なアクションプラン(優先度順) + +### STEP 1: iOSアプリリリース直前・直後(〜リリース後1週間) + +| # | アクション | 担当 | 工数 | 期待効果 | +|---|-----------|------|------|---------| +| 1 | Smart App Bannerをすべてのページに設置 | 開発 | 1時間 | 月10,000インプレの1〜3%をApp Storeへ誘導 | +| 2 | App Store掲載情報の最適化(タイトル・サブタイトル・説明文・キーワード) | マーケ | 2〜3時間 | 検索流入の基盤構築 | +| 3 | スクリーンショット5枚を本施策書の設計に基づき作成 | デザイン | 3〜5時間 | CVR向上 | +| 4 | アプリ内レビュー依頼ダイアログの実装(成績入力5回目に表示) | 開発 | 2〜3時間 | レビュー数の早期積み上げ | +| 5 | X公式アカウント開設・リリース告知投稿 | マーケ | 1時間 | 認知拡大 | +| 6 | 既存Webユーザーへのアプリリリース通知(サイト内バナー) | 開発 | 2〜3時間 | 既存800人をアプリへ誘導 | + +### STEP 2: リリース後2〜4週間 + +| # | アクション | 担当 | 工数 | 期待効果 | +|---|-----------|------|------|---------| +| 7 | 計算ツールページに「アプリで成績を継続管理」CTAバナーを設置 | 開発+デザイン | 3〜5時間 | 月10,000インプレからアプリへの転換 | +| 8 | Instagram公式アカウント開設・初期投稿10件 | マーケ | 5〜8時間 | SNSチャネルの基盤構築 | +| 9 | Xでの野球コミュニティとの交流開始(いいね・リプライ・引用RT) | マーケ | 週2〜3時間 | フォロワー獲得・認知拡大 | +| 10 | PR TIMESでプレスリリース配信(無料枠) | マーケ | 2〜3時間 | メディア露出、被リンク獲得 | + +### STEP 3: リリース後1〜3ヶ月 + +| # | アクション | 担当 | 工数 | 期待効果 | +|---|-----------|------|------|---------| +| 11 | 新規計算ツール追加(防御率・WHIP・得点圏打率等) | 開発 | 5〜10時間/ツール | SEO流入のさらなる拡大 | +| 12 | App Store掲載情報のA/Bテスト(サブタイトル・説明文のバリエーション) | マーケ | 1時間/テスト | CVRの改善 | +| 13 | 初レビュー50件到達時にSNSで感謝投稿 | マーケ | 1時間 | コミュニティ醸成 | +| 14 | 夏の大会シーズン連動コンテンツの事前準備(6月中に) | マーケ | 3〜5時間 | 7〜8月の最重要シーズンへの備え | +| 15 | TikTokアカウント開設を検討(Instagram 300フォロワー達成後) | マーケ | - | チャネル拡大 | + +### STEP 4: 夏の大会シーズン(7〜8月) + +| # | アクション | 期待効果 | +|---|-----------|---------| +| 16 | 「夏の大会の成績をBUZZ BASEに記録しよう」キャンペーン | 年間最大の獲得機会 | +| 17 | 甲子園開催中の連動コンテンツ投稿(甲子園出場選手の成績指標解説等) | リーチ最大化 | +| 18 | 中学生新チーム(8月始動)向けの「新チームで始めよう」施策 | 中学生ユーザー獲得 | +| 19 | アプリのシーズン成績サマリー機能リリース(可能であれば) | リテンション向上・シェア促進 | + +--- + +## KPIとチェックポイント + +### アプリDL数目標 + +| 時点 | 目標累計DL数 | 主な成長ドライバー | +|------|------------|----------------| +| リリース後1ヶ月 | 100〜200 | 既存Webユーザーの移行 + Smart App Banner | +| リリース後3ヶ月 | 300〜500 | SNS運用 + 計算ツールCTA | +| リリース後6ヶ月(9月) | 500〜1,000 | 夏の大会連動 + 口コミ | + +### App Store指標の週次確認項目 + +- インプレッション数(App Storeでの表示回数) +- プロダクトページビュー数 +- DL数 +- 転換率(インプレッション→DL) +- 評価スコア・レビュー件数 + +**転換率の目安**: 通常1〜3%。5%以上なら優秀。転換率が低い場合はスクリーンショット・説明文を見直す。 + +### SNS指標の週次確認項目 + +- フォロワー数の推移 +- 最もエンゲージメントの高かった投稿(内容・形式を分析して再現) +- プロフィールリンクのクリック数 + +--- + +## 参考: 競合アプリの現状 + +現在App Storeで「野球 成績」「野球 個人成績」で検索した際の主な競合状況: + +| アプリ名 | 強み | BUZZ BASEの差別化ポイント | +|---------|------|------------------------| +| ヒットメーカー | 45,000DL、打撃成績のセイバーメトリクス分析が充実 | ランキング・グループ機能がない。BUZZ BASEはソーシャル要素が強み | +| .333 | シンプルな打撃成績記録 | 機能が限定的。BUZZ BASEはOPS自動計算・ランキング・野球ノートなど多機能 | +| TeamHub | 40万チーム導入、チーム管理が充実 | チーム管理ツール。BUZZ BASEは個人成績のランキング・SNS的共有が差別化 | + +**BUZZ BASEが狙うべきポジション**: 「成績の記録・計算」だけでなく「チームメイトとの成績比較・競争・共有」という体験を提供する唯一のアプリ。この価値をASOのすべての場所で一貫して訴求する。 + +--- + +*本施策書は `docs/strategy/marketing/growth-plan.md` および `docs/strategy/pdca/cycle1-plan.md` と連携して運用する。* +*次回レビュー: iOSアプリリリース後1ヶ月時点* diff --git a/docs/strategy/marketing/web-traffic-to-app-202604.md b/docs/strategy/marketing/web-traffic-to-app-202604.md new file mode 100644 index 0000000..14aa858 --- /dev/null +++ b/docs/strategy/marketing/web-traffic-to-app-202604.md @@ -0,0 +1,702 @@ +# Web流入からiOSアプリ登録増加 施策書 + +**作成日**: 2026年4月30日 +**前提データ**: 2026年4月1日〜4月29日 GA4 / Search Console +**目的**: Web月間1,700セッションをiOSアプリDL・アカウント作成に転換する + +--- + +## 現状サマリー + +| 指標 | 4月実績 | 前回比較(2〜3月) | +|------|---------|-----------------| +| アクティブユーザー | 1,162 | +38%↑ | +| セッション | 1,723 | +106%↑ | +| 新規率 | 94.6% | 横ばい | +| Google Organic | 864セッション(50.1%) | +49%↑ | +| SNS (t.co) | 5セッション(0.3%) | ほぼゼロ | +| AI検索(ChatGPT/OpenAI/Gemini) | 4セッション | 立ち上がり始め | +| App Store発信クリック | 約85件 | 初計測 | + +### ファネルの現状 + +``` +SC表示: 推定170,000回/月(上位クエリの積算) + ↓ 平均CTR: 約1% +クリック(セッション): 1,723 + ↓ 推定セッション→App Storeクリック率: 約5% +App Storeページ流入: 85件/月 + ↓ App Store CVR: 推定15〜25% +DL推定: 13〜21件/月 + ↓ アカウント作成率: 不明(要計測) +新規アカウント: 不明 +``` + +### 最大ボトルネックの特定 + +1. **計算ツール系のCTR(1〜1.4%)が極低**: ops計算(5,544表示で77クリック)、obp計算(2,919表示で33クリック)。表示は大量にあるがクリックされない = titleとdescriptionが検索意図に合っていない +2. **Web→App Store転換率が低い**: 1,723セッションに対してApp Store発信クリック85件 = 転換率4.9%。計算ツール着地ユーザーの大半がアプリ誘導なしで離脱 +3. **SNSがほぼゼロ**: 5セッション/月。オーガニック流入に依存し、バイラル経路が存在しない + +--- + +## 施策1: SEO Quick-wins 実行計画 + +### 1-1. 上位10クエリのtitle/description/H1改善案 + +Quick-win候補33件のうち、インプレッション規模と現在順位(6〜8位圏)を考慮して上位10件に絞る。 + +#### 1位: 「長打率 計算」(2,836インプレ、順位5.9、CTR推定0.3%) +期待追加クリック: +138/月 + +| 要素 | 現状(推定) | 改善案 | +|------|------------|--------| +| title | 長打率計算 - BUZZ BASE | 長打率の計算方法と自動計算ツール【野球成績管理アプリ】 | +| description | - | 長打率(SLG)を自動計算。単打・二塁打・三塁打・本塁打を入力するだけで即算出。BUZZ BASEなら毎試合の成績を記録してシーズン全体の長打率も管理できます。 | +| H1 | 長打率計算 | 長打率(SLG)自動計算ツール | +| コンテンツ追加 | なし | 「長打率の目安(平均値・好打者の基準)」セクションを計算フォームの下に追加 | +| アプリCTA | 弱い | 計算結果表示後に「毎試合の長打率をアプリで自動管理」バナー表示 | + +#### 2位: 「k/bb」(2,424インプレ、順位6.5、CTR推定0.5%) +期待追加クリック: +119/月 + +| 要素 | 現状(推定) | 改善案 | +|------|------------|--------| +| title | K/BB計算 - BUZZ BASE | K/BB(奪三振÷与四球)計算ツールと目安【投手成績】 | +| description | - | K/BBを自動計算。三振数と四球数を入力するだけで即算出。K/BBとは何か・目安値の解説も掲載。BUZZ BASEアプリで毎試合の投手成績を記録できます。 | +| H1 | K/BB計算 | K/BB自動計算ツール(奪三振÷与四球) | +| コンテンツ追加 | なし | 「K/BBとは」「目安値(2.0以上が優秀など)」セクションを追加 | + +#### 3位: 「k/bbとは」(2,024インプレ、順位5.2、CTR 0%) +期待追加クリック: +101/月。現在0クリックは「解説コンテンツが存在しない」が原因。 + +| 要素 | 改善案 | +|------|--------| +| 対応方法 | /tools/k-bb ページに「K/BBとは」セクションを追加(別ページ不要、既存ページの強化で対応) | +| セクション構成 | ①K/BBの定義(奪三振数÷与四球数)→②なぜ重要か(コントロールと奪三振力の両立指標)→③目安値(1.5未満/2.0以上/3.0以上のランク)→④プロ選手の実例 | +| 構造化データ | FAQSchemaで「K/BBとは?」「K/BBの目安は?」を追加 | +| H2 | K/BBとは?野球の投手指標をわかりやすく解説 | + +#### 4位: 「失点」(668インプレ、順位6.2) +期待追加クリック: +33/月 + +| 要素 | 改善案 | +|------|--------| +| title | 失点とは【野球用語解説】自責点との違いと計算方法 - BUZZ BASE | +| description | 野球の「失点」と「自責点」の違いを解説。失点・自責点から防御率(ERA)を自動計算できます。BUZZ BASEで投手成績を記録・管理。 | +| コンテンツ | 「失点と自責点の違い」「失点から防御率を計算する方法」を解説セクションとして追加 | + +#### 5位: 「ops 目安」(529インプレ) +期待追加クリック: +26/月 + +| 要素 | 改善案 | +|------|--------| +| 対応ページ | /tools/ops に「OPSの目安」セクションを追加 | +| コンテンツ | 打者のランク表(0.600未満/0.700/0.800/0.900/1.000以上)と実際のプロ選手のOPS事例 | +| title | OPS計算ツール+目安ランク一覧【野球成績】 - BUZZ BASE | + +#### 6位: 「obp 野球」(441インプレ) +期待追加クリック: +22/月 + +| 要素 | 改善案 | +|------|--------| +| title | 出塁率(OBP)計算ツール+野球での意味と目安 - BUZZ BASE | +| description | 出塁率(OBP)を自動計算。安打・四球・死球・打数を入力するだけ。OBPとは何か・目安値・OPSとの関係も解説。BUZZ BASEで成績管理。 | +| H2追加 | OBPとは?野球における出塁率の重要性 | + +#### 7位: 「野球 指標 一覧」(/calculation-of-grades、順位6.9) +期待追加クリック: +14/月 + +| 要素 | 改善案 | +|------|--------| +| title | 野球の成績指標一覧【打率・OPS・防御率・WHIP等】計算方法まとめ - BUZZ BASE | +| description | 野球の主要成績指標(打率・出塁率・長打率・OPS・防御率・WHIP・K/BBなど)の計算方法と目安値を一覧で解説。各指標の自動計算ツールも提供。 | +| H1 | 野球の成績指標・計算方法まとめ一覧 | +| コンテンツ | 各指標への内部リンクを表形式で整理。相互リンクでサイト内回遊を促進 | + +#### 8位: 「打率計算アプリ」(181インプレ、CTR 2.8%) +順位は高くないが、「アプリ」を含む高インテントクエリのため優先。 + +| 要素 | 改善案 | +|------|--------| +| title | 打率計算アプリ【無料】チームでランキング比較 - BUZZ BASE | +| description | 打率を自動計算して毎試合記録できる無料アプリ。チームメイトと打率をランキングで比較する機能つき。App Storeから無料ダウンロード。 | +| CTA | モバイル着地時にApp Storeリンクを目立つ位置に配置 | + +#### 9位: 「野球 成績 アプリ」(121インプレ、CTR 7.4%、順位5.5) +CTRが既に高い。順位を5位→3位圏に上げることで大幅増が見込める。 + +| 要素 | 改善案 | +|------|--------| +| title | 野球 個人成績アプリ【無料】チームでランキング共有 - BUZZ BASE | +| description | 打率・OPS・防御率などを自動計算して、チームメイトとランキングで成績を比較できる無料アプリ。中高生野球部員1,000人以上が利用。App Storeでダウンロード。 | +| 内部リンク強化 | 計算ツール全ページからTOPページへの誘導強化 | + +#### 10位: 「野球 個人成績 アプリ 無料」(357インプレ、CTR 15%、順位3.8) +既に高CTR。順位を3位→1位に上げることが最優先。 + +| 要素 | 改善案 | +|------|--------| +| title | 野球 個人成績アプリ 無料ダウンロード【BUZZ BASE】チームでランキング | +| description | 野球の個人成績をアプリで記録・管理。打率・OPS・防御率が自動計算され、チーム内ランキングで成績を比較できます。完全無料。App Storeで配信中。 | +| 施策 | TOPページのE-E-A-T強化(利用者数、開発者プロフィール、レビュー数を追記) | + +### 1-2. 独立ページ新設の是非 + +**結論: 現状は既存ページへのセクション追加で対応。独立ページ化は月間クリック50件超えを目安に判断する。** + +理由: +- 現在の /tools/k-bb、/tools/ops はURL・ドメインオーソリティが蓄積済み +- 「k/bbとは」「ops 目安」は既存ページにセクション追加することで共食いなく順位を取れる +- ページを分割すると内部リンクの管理コストが増加し、個人開発では維持困難 +- ただし「野球 指標 一覧」のような横断的なクエリは /calculation-of-grades を強化する方向で対応 + +独立ページ化を検討するタイミング: +- 「k/bbとは」単体で月間クリック50件を超えたとき +- コンテンツが2,000文字を超えて既存ページの構造を壊すとき + +### 1-3. 「k/bbとは」解説ページ新設提案(実質セクション追加) + +/tools/k-bb ページに以下のセクションを計算フォームの下に追加する。 + +``` +## K/BBとは? +K/BB(ケーバイビービー)は投手の奪三振数(K)を与四球数(BB)で割った指標です。 +数値が高いほど「三振を奪いながら四球を出さない」優れた投手であることを示します。 + +## K/BBの計算式 +K/BB = 奪三振数 ÷ 与四球数 + +## K/BBの目安 +| K/BB | 評価 | +|------|------| +| 1.5未満 | 要改善(コントロールに課題) | +| 1.5〜2.0 | 普通 | +| 2.0〜3.0 | 良好 | +| 3.0以上 | 優秀 | +| 5.0以上 | エリート | + +## よくある質問 +Q: K/BBが高すぎても問題ある? +A: 基本的に高いほど良いですが、奪三振を狙いすぎて球数が増える場合は注意が必要です。 + +Q: プロ野球選手のK/BBはどのくらい? +A: NPBの優良先発投手は2.5〜4.0程度。3.0を超えると上位クラスです。 +``` + +FAQSchema構造化データを合わせて実装することでAI検索・featured snippetへの対応も強化する。 + +### 1-4. LLMフレンドリーな構造化データ・FAQ強化案 + +AI検索(ChatGPT、Perplexity、Gemini)からの流入が4月に立ち上がり始めた(4セッション)。今後3〜6ヶ月で急増する前に構造的な対応を行う。 + +**実装優先度: A / 工数: 各ページ30分〜1時間** + +#### (1) FAQSchema の全計算ツールページへの実装 + +各計算ツールページに以下のFAQSchemaを追加する。 + +```json +{ + "@context": "https://schema.org", + "@type": "FAQPage", + "mainEntity": [ + { + "@type": "Question", + "name": "OPSの計算式は?", + "acceptedAnswer": { + "@type": "Answer", + "text": "OPS = 出塁率(OBP)+ 長打率(SLG)。出塁率は(安打+四球+死球)÷(打数+四球+死球+犠飛)、長打率は(塁打数)÷打数で計算します。" + } + }, + { + "@type": "Question", + "name": "OPSの目安は?", + "acceptedAnswer": { + "@type": "Answer", + "text": "0.600未満は要改善、0.700以上は平均的、0.800以上は好打者、0.900以上は一流打者、1.000以上はエリートレベルです。" + } + } + ] +} +``` + +#### (2) WebApplicationSchema を計算ツールに追加 + +```json +{ + "@context": "https://schema.org", + "@type": "WebApplication", + "name": "OPS計算ツール - BUZZ BASE", + "description": "野球の総合打撃指標OPSを自動計算するツール", + "applicationCategory": "SportsApplication", + "operatingSystem": "Web", + "offers": { + "@type": "Offer", + "price": "0" + } +} +``` + +#### (3) コンテンツの「LLMが引用しやすい形式」への最適化 + +- 各指標の定義を「〜とは、〜のことです」という断言形式で冒頭に置く +- 目安値は必ず数値入りの表で示す(LLMが数値情報を抽出しやすい) +- 「よくある質問」セクションを全ページに追加(LLMがFAQ形式を好む) +- BUZZ BASEアプリの言及を自然に組み込む(「BUZZ BASEアプリでは〜を自動計算できます」) + +--- + +## 施策2: SNS(特にX)戦略 + +### 2-1. 中高生野球部員のXコミュニティ攻略法 + +**現状**: t.co(X)から月5セッション。既存ドキュメントでX運用を推奨しているが未着手の可能性が高い。 + +#### フォロワー獲得経路(ゼロからの立ち上げ) + +**Phase 1(最初の30日): コミュニティへの潜入** + +1. 「#野球垢さんと繋がりたい」「#高校野球」「#野球部」で毎日10〜20アカウントをフォロー +2. 野球の成績・練習に関する投稿(特に高いエンゲージメントのもの)に有益なリプライを送る(「その打率ならOPSは〇〇くらいですね」等の具体的コメント) +3. 甲子園・大会情報を引用RTしてコメントを添える(時事コンテンツで露出を稼ぐ) +4. 初期30日は「フォロワーを得る」より「存在を知ってもらう」を優先 + +**効果的なハッシュタグ(4月〜7月シーズン向け)** + +``` +基本セット(毎回): +#野球 #野球部 #高校野球 + +カテゴリ別(投稿内容で選択): +成績系: #打率 #OPS #成績管理 #野球指標 +中学野球: #中学野球 #シニア #ボーイズ #ポニー +大会系: #春季大会 #夏の大会 +あるある系: #野球あるある #野球バカ #球児 +``` + +### 2-2. 投稿コンテンツ案 + +**週間投稿ローテーション(週4〜5投稿)** + +| 曜日 | テーマ | 具体的投稿案 | +|------|--------|------------| +| 月 | 成績Tips(知識提供) | 「OPSが.800以上の打者は好打者の目安。計算式: 出塁率+長打率。あなたのOPSは何割? #野球 #OPS」 | +| 水 | 野球あるある | 「試合終わってすぐ打率を計算する野球部員あるある。10打数3安打で打率.300って言いたいだけ #野球あるある」 | +| 金 | アプリ紹介(週1回以内) | 画像付きでアプリ機能紹介。「成績を記録してチームメイトとランキング比較ができます」 | +| 土 | 大会・時事連動 | 週末の大会情報引用RT + コメント「この試合の成績をBUZZ BASEで記録すると防御率は〜」 | +| (随時) | ユーザーの投稿リポスト | #BUZZBASE タグ付き投稿をRT、ありがとう一言添える | + +**すぐ使える投稿テンプレート5本** + +``` +【テンプレート1: 計算Tipsシリーズ】 +野球の成績指標、今更聞けない基礎知識 + +「K/BB」って何かわかる? + +奪三振数÷与四球数 +2.0以上なら優秀な投手の目安 + +打者は打率だけじゃなくてOPS +投手はK/BBも見てみると +自分の成績の見え方が変わるよ + +#野球 #野球部 #投手成績 #K/BB +``` + +``` +【テンプレート2: あるあるシリーズ】 +野球部の成績気にする人あるある + +①試合終わってすぐ打率計算 +②チームメイトの打率も計算 +③「今日ノーヒットだけど前の試合3安打だから…」って言い訳を計算する +④監督よりも自分の成績を把握してる + +全部該当したら本物の成績マニア +#野球あるある #野球部 #高校野球 +``` + +``` +【テンプレート3: 質問シリーズ】 +野球選手のみなさんに聞きたいんですが + +今シーズンの目標打率ってどのくらい設定してますか? + +○ 2割5分以上 +○ 3割以上 +○ 3割5分以上 +○ とにかく全打席ヒット + +#野球 #野球部 +(投票機能を使う) +``` + +``` +【テンプレート4: 大会連動シリーズ】 +夏の大会まで残り〇〇日 + +今のうちに毎試合の成績を記録しておくと +大会前後でどれだけ成長したか数字で見える + +「去年の俺より打ってる」が証明できる +#高校野球 #夏の大会 #野球部 +``` + +``` +【テンプレート5: アプリCTAシリーズ(月2〜3回まで)】 +チームで誰が一番打ってるか +数字で証明したい野球部員へ + +BUZZ BASEのグループ機能を使うと +チームメイんとランキングで成績比較できます + +・打率・OPS・防御率を自動計算 +・グループ内リアルタイムランキング +・完全無料 + +App Store: [URL] +#野球 #野球部 #高校野球 +``` + +### 2-3. X→アプリDLへの動線設計 + +**プロフィール設定** + +``` +名前: BUZZ BASE(野球成績管理アプリ) +Bio: +野球の個人成績をランキングで共有するアプリ📊 +打率・OPS・防御率を自動計算 / チームメイトと成績比較 / 完全無料 +⬇️App Storeでダウンロード +buzzbase.jp + +固定ポスト(ピン留め): +アプリの機能紹介 or 最も反響があった投稿を固定 +→ 「何ができるか」が2秒でわかるものを選ぶ +→ 成績カード画像 + App Storeリンク + 「無料ダウンロード」テキスト +``` + +**画像内CTA** + +- 成績Tips系の画像投稿の最下部に「BUZZ BASEで計算 buzzbase.jp」を小さく入れる(広告感を出しすぎない) +- アプリ紹介投稿はスクリーンショット画像の上に「タップしてダウンロード」矢印テキストを重ねる + +### 2-4. 既存Webユーザー(1,000人超)からのバイラル獲得施策 + +**成績シェア機能活用(最重要・プロダクト施策との連携)** + +4月の分析で「App Store発信クリック85件のうちTOP 48件」という結果は、TOPページ着地ユーザーがアプリ誘導に反応していることを示す。計算ツール着地ユーザー(月1,400セッション超)への展開が急務。 + +| 施策 | 内容 | 期待効果 | +|------|------|---------| +| 成績カード画像生成 | 個人成績をシェア可能な画像で生成(既存ドキュメントで実装提案済み) | ユーザーが自発的にXでシェア → 新規流入 | +| 「シーズン成績サマリー」ボタン | 蓄積した成績を1枚の画像に集約してシェア | 年間最大のUGC発生ポイント | +| 計算ツール結果のシェアボタン | 計算結果を「俺のOPSは.850でした」とワンタップでXシェア | 計算ツールユーザーをバイラル起点に転換 | + +--- + +## 施策3: コンテンツマーケティング + +### 3-1. SC上位クエリから逆算した記事企画10本 + +各記事の主目的は「計算ツールへの内部誘導 → アプリDL」。独立ブログ記事ではなく、既存ツールページのコンテンツ拡充 + 新規解説ページの組み合わせで対応する。 + +| 記事企画 | 対象ページ | 想定月間クリック | 記事内DLゴール設定 | +|---------|----------|---------------|-----------------| +| 1. K/BBとは?投手の制球力と奪三振力を示す指標の解説と目安 | /tools/k-bb に追加 | +120/月 | 記事内に「K/BBを自動計算 + アプリで毎試合記録」CTA | +| 2. 長打率(SLG)の計算方法・目安・OPSとの違い | /tools/slg を新設 or /tools/ops に追加 | +138/月 | 「長打率を自動計算できるアプリをダウンロード」 | +| 3. OPSの目安ランク一覧と日本プロ野球選手の実例 | /tools/ops に追加 | +26/月 | 計算後に「あなたのOPSをチームで比較しませんか?」 | +| 4. 出塁率(OBP)の意味・計算方法・目安と打率との違い | /tools/obp に追加 | +22/月 | 「出塁率をチームでランキング比較できます」 | +| 5. 野球の成績指標まとめ:打者・投手・守備の主要指標一覧 | /calculation-of-grades を拡充 | +14/月(ハブとして他流入の起点) | ページ内の各ツールへ誘導 → ツールからアプリDL | +| 6. 防御率(ERA)の計算方法と目安 | /tools/era を新設 or 既存強化 | 推定+50/月(新規キーワード) | 「投手成績をBUZZ BASEで記録しよう」 | +| 7. WHIP(投手指標)の意味・計算方法・目安と防御率との違い | /tools/whip に追加 | 推定+40/月 | 「WHIP・防御率・K/BBをまとめて管理するアプリ」 | +| 8. 打率3割を達成するための成績管理法 | /blog/batting-average-300(新規) | 推定+80/月 | 記事全体が「BUZZ BASEを使って成績管理した」前提で書く | +| 9. 野球の自主練成績を記録するメリット【データで成長する方法】 | /blog/practice-record(新規) | 推定+30/月 | アプリの野球ノート機能への誘導 | +| 10. チームで誰が一番打ってる?成績をランキングで比較する方法 | /blog/team-ranking(新規) | 推定+40/月 | グループ機能→アプリDLへの直接誘導記事 | + +**UGC的訴求記事の具体案(記事10の設計)** + +「チームで誰が一番打ってる?」記事は、読者を「チームメイントの成績を比較したい」という欲求で引き込み、BUZZ BASEのグループ機能で解決するストーリー構成にする。 + +``` +記事構成: +1. リード: 「野球部の成績議論、絶対に起こりますよね」(共感) +2. 問題提起: 「でも口頭での成績比較はあてにならない」 +3. 解決策提示: 「成績を数字で管理・比較できるアプリがある」 +4. BUZZ BASEの機能紹介: グループ作成→成績入力→ランキング表示の流れ +5. CTA: 「今すぐApp Storeでダウンロード(無料)」 +``` + +--- + +## 施策4: 集客チャネルの優先順位 + +### 4月現状のチャネル構成と課題 + +| チャネル | 4月セッション | シェア | 課題 | +|---------|------------|--------|------| +| Google Organic | 864 | 50.1% | CTRが低い(平均1%)。Quick-winで改善余地大 | +| Yahoo Organic | 382 | 22.2% | Google改善施策がYahooにも波及するためコスパ高 | +| Direct | 285 | 16.6% | ブランド認知の蓄積。維持・向上が必要 | +| Bing | 156 | 9.1% | 想定外に大きい。Bing向けの最適化は現状不要(放置でOK) | +| SNS (t.co) | 5 | 0.3% | 実質ゼロ。今後の成長余地が最大 | +| AI検索 | 4 | 0.2% | 立ち上がり段階。今のうちに構造化対応が必要 | + +### リソース配分の推奨 + +**現状 Organic 80%依存は短期的に正しい判断。ただし以下の優先順位で多様化する。** + +| 優先度 | チャネル | 推奨リソース配分 | 理由 | +|-------|---------|---------------|------| +| S | SEO(既存ページ改善) | 月4〜6時間 | 既に流入の72%。CTR改善だけで+300〜500クリック/月が狙える | +| A | X(運用開始) | 月4〜5時間 | コスト最低。野球垢コミュニティへのリーチが可能 | +| A | AI検索対応(FAQ/構造化データ) | 月2〜3時間 | 今から仕込むことで6ヶ月後に効いてくる | +| A | コンテンツ(記事拡充) | 月4〜6時間 | SEOの長期的な資産を積み上げる | +| B | TikTok | 月6〜8時間 | 中学生への最強リーチだが動画制作工数がかかる | +| B | Instagram | 月2〜3時間 | TikTok動画転用で工数圧縮 | +| C | YouTube | 投資見送り | 工数対効果が低い。TikTok・Instagram安定後に検討 | + +### YouTube/TikTok/Instagramの判断基準 + +**TikTokは5月に開始を推奨する。理由は以下の3点。** + +1. 4月の大学生新チーム始動(春季大会シーズン)と5月〜7月の大会シーズンが最もコンテンツが作りやすい時期 +2. フォロワー0からでもアルゴリズムで拡散されるため、早期開始のコスト < 遅延のコスト +3. 動画制作はCapCut + アプリ画面録画 + テキストオーバーレイで1本15〜20分で制作可能 + +**Instagramはコンテンツリパーパシング(TikTok動画の転用)で月2〜3時間の追加工数のみ。** + +YouTubeは月間アプリDL 100件を超えてから検討する。 + +--- + +## 施策5: Web→アプリ登録へのCVR改善 + +### 5-1. 計算ツール系のApp Store CTRを1〜3%から5%以上へ + +4月の実績では計算ツールページ(/tools/obp、/tools/ops)からのApp Store発信クリックが各6件。月間ページビューが数千単位であることを考えると転換率0.1〜0.2%程度。これを3〜5%に改善する。 + +#### 実装施策(優先度順) + +**A. 計算結果直後のCTAバナー(最優先)** + +計算ボタンを押して結果が表示されたタイミングでのみ表示する。 + +``` +------------------------------------------- +この成績を毎試合記録しませんか? +BUZZ BASEアプリなら打率・OPS・出塁率が +試合ごとに自動で計算・記録されます。 + +チームメイトとランキングで比較もできます。 +[App Storeで無料ダウンロード ↗] +------------------------------------------- +``` + +コピーの設計意図: +- 「この成績を」という表現で計算したばかりの成績と紐付ける(即時性) +- 「毎試合」という頻度を示すことで継続価値を訴求 +- 「ランキング比較」という差別化機能を必ず含める +- 「無料」を明記 + +**B. スマホ/PCでの出し分け** + +``` +モバイル(Safari)表示: +→ Smart App Bannerが画面上部に自動表示(実装済み) +→ CTAボタンは「App Storeでダウンロード(無料)」のみ + +モバイル(Chrome)表示: +→ CTAバナーにApp Storeリンクを大きく表示 + +PC表示: +→ CTAバナーにQRコード + 「スマホで読み込んでApp Storeへ」 +``` + +**C. ソーシャルプルーフの追加** + +計算ツールページのアプリCTAに「1,000人以上が利用中」「App Store ★4.5」を追加する(レビュー数が増え次第数値を更新)。 + +**D. ページ上部のApp Store導線(ファーストビュー)** + +現状、計算ツールページのファーストビューにはアプリ誘導がない(計算フォームのみ)。ページ上部に小さいバナーを追加する。 + +``` +[App Storeで成績を継続管理する →] +``` + +幅の狭いバナーを計算フォームの直上に配置。ユーザーの計算意欲を妨げないサイズ感で。 + +### 5-2. TOPページのアプリ訴求LP化の是非 + +**結論: TOPページの構成変更は推奨する。ただし全面LP化は不要。「アプリファースト」の見せ方に変更する程度で十分。** + +現状の「野球 個人成績 アプリ 無料」検索(CTR 15%、月54クリック)は、アプリを探しているユーザーが着地している。このユーザーに対してTOPページが「アプリがある」ことを即座に伝えられているか確認・改善する。 + +**TOPページのアプリ訴求強化案** + +``` +ファーストビューに以下を追加: +- App Storeへのリンクボタン(モバイル優先) +- アプリのスクリーンショット1枚 +- 「無料ダウンロード」の文字 + +ファーストビューの次のセクション: +- 主要機能を3つのアイコン+テキストで説明 +- 「1,000人の野球選手が使っています」等のソーシャルプルーフ +``` + +**全面LP化しない理由**: +- 現在のTOPはWebアプリ自体への登録ページとしても機能している +- Web版を使い続けているユーザー(約5%)もおり、彼らの離脱を招くリスクがある +- 「アプリを推しながらWeb版も使える」の共存が最適 + +--- + +## 優先度・工数・期待効果サマリー + +| 優先度 | 施策 | 工数 | 期待効果(DL +X/月) | KPI | +|-------|------|------|-------------------|-----| +| S | 計算ツール結果直後のCTAバナー追加 | 0.5日 | +8〜15 DL/月 | App Store発信クリック: 85→150件/月 | +| S | 計算ツール上位10件のtitle/description改善 | 3〜4時間 | +200〜400クリック/月 | 対象クエリのCTR: 1%→3%以上 | +| S | K/BB・長打率・OPSページへの解説セクション追加 | 4〜6時間 | +150〜250クリック/月 | 「k/bbとは」クエリのクリック: 0→50件/月 | +| A | FAQSchema・WebApplicationSchema実装 | 2〜3時間 | AI検索流入: 4→20件/月(3ヶ月後) | AI検索セッション数 | +| A | Xアカウント運用開始(週4〜5投稿) | 4〜5時間/月 | +10〜20 DL/月(3ヶ月後) | X発セッション: 5→50件/月 | +| A | TikTokアカウント開設・週3本投稿 | 6〜8時間/月 | +20〜40 DL/月(3ヶ月後) | TikTokフォロワー: 0→500(3ヶ月) | +| A | TOPページのアプリ訴求強化(ファーストビュー) | 2〜3時間 | +5〜10 DL/月 | TOP→App Store CTR: 11%→15%以上 | +| B | 計算ツールページ上部への小型CTAバナー | 1〜2時間 | +3〜8 DL/月 | tools系 App Store CTR: 0.1%→0.5% | +| B | 「打率3割を達成するための成績管理法」記事新設 | 4〜6時間 | +30〜60クリック/月(2〜3ヶ月後) | 月間セッション増加数 | +| B | 「チームランキング比較」記事新設(アプリ直接誘導) | 4〜6時間 | +5〜10 DL/月(2〜3ヶ月後) | 記事→App Store CTR | +| B | 成績カード画像生成&SNSシェア機能(プロダクト) | 3〜4日 | +15〜30 DL/月(機能リリース後) | シェア数、シェア経由DL数 | +| C | Instagram(TikTok転用のみ) | 2〜3時間/月 | +5〜10 DL/月(3ヶ月後) | Insta発セッション数 | + +--- + +## 3ヶ月実行ロードマップ + +### 5月(シーズン最重要期・春季大会終盤〜夏の大会準備) + +**テーマ: SEO基盤を固め、SNSを立ち上げる** + +| 週 | 施策 | 優先度 | 工数 | +|----|------|--------|------| +| 第1週 | 計算ツール上位10件のtitle/description改善を実装 | S | 3〜4時間 | +| 第1週 | K/BB・長打率ページに解説セクション追加 + FAQSchema実装 | S+A | 4〜5時間 | +| 第2週 | 計算ツール結果直後のCTAバナー実装 | S | 0.5日 | +| 第2週 | Xアカウント開設、プロフィール設定、初期投稿10件 | A | 3〜4時間 | +| 第2週 | TikTokアカウント開設、第1投稿(野球あるある系) | A | 2〜3時間 | +| 第3週 | OPS・OBPページに「目安ランク」セクション追加 | A | 2〜3時間 | +| 第3週 | X投稿の定常運用(週4〜5投稿)開始 | A | 継続 | +| 第3週 | TikTok週3本体制に移行 | A | 継続 | +| 第4週 | TOPページのファーストビューにApp Store CTAを追加 | A | 2〜3時間 | +| 第4週 | 5月のSEO・SNS効果を計測・レビュー | - | 1〜2時間 | + +5月末目標: App Store発信クリック 85→150件/月, Xフォロワー0→50人 + +### 6月(夏の大会予選まで約1ヶ月・機能開発期) + +**テーマ: バイラル機能を実装し、口コミの種を仕込む** + +| 週 | 施策 | 優先度 | 工数 | +|----|------|--------|------| +| 第1〜2週 | 成績カード画像生成&SNSシェアボタン実装 | B | 3〜4日 | +| 第2週 | 「夏の大会に向けて成績を記録しよう」SNSキャンペーン開始 | A | 2時間 | +| 第3週 | 計算ツールページ上部への小型CTAバナー追加 | B | 1〜2時間 | +| 第3週 | 防御率(ERA)・WHIPページのコンテンツ強化 | B | 2〜3時間 | +| 第4週 | 「チームランキング比較」記事の公開 | B | 4〜6時間 | +| 全週 | X週4〜5投稿、TikTok週3本の定常運用継続 | A | 継続 | + +6月末目標: App Store発信クリック 200件/月, Xフォロワー 100人, TikTokフォロワー 200人 + +### 7月(夏の大会予選・最重要シーズン・年間最大のチャンス) + +**テーマ: シーズン連動コンテンツで最大獲得を狙う** + +| 週 | 施策 | 優先度 | 工数 | +|----|------|--------|------| +| 第1週 | 「最後の夏の成績を記録しよう」SNS集中投稿(5本以上) | S | 3〜4時間 | +| 第1〜2週 | グループ招待URLの強化実装(URL形式に変更) | S | 3〜4日 | +| 第2週 | 夏の大会連動TikTok動画(感情系コンテンツ) | A | 3〜4時間 | +| 第3週 | 甲子園開幕に合わせたSNS連動コンテンツ | A | 2〜3時間 | +| 第4週 | 夏の大会結果を踏まえた「9月の新チーム始動」施策の事前計画 | B | 2時間 | +| 全週 | X週4〜5投稿、TikTok週3〜5本(大会シーズンは増量) | S | 継続 | + +7月末目標: App Store発信クリック 300件/月以上, DL推定 50件/月, Xフォロワー 200人, TikTokフォロワー 500人 + +--- + +## 今週やること(5月第1週)厳選3つ + +### 1位(最優先): 計算ツール上位10件のtitle/description改善 + +**理由**: 既に月2,800〜5,500インプレッションがあるページのCTRを1%から3%に改善するだけで、即座に月+100〜300クリックの増加が見込める。コスト0、工数3〜4時間で最大のリターンが得られる施策。 + +**具体的アクション**: +1. /tools/k-bb: titleに「K/BBとは」「目安」を含める。H2で「K/BBとは?」セクションを追加 +2. /tools/slg(長打率): 「長打率 計算」に最適化したtitle/descriptionに変更 +3. /tools/ops: 「ops 目安」セクションを追加してdescriptionに「目安ランク一覧」を含める +4. /tools/obp: 「obp 野球」に最適化したdescriptionに変更 + +### 2位: 計算結果直後のアプリCTAバナー実装 + +**理由**: 月1,400件以上の計算ツールセッションがアプリ誘導なしで離脱している。計算後の「成功体験直後」というCTAの最適タイミングを逃している。半日の工数でApp Store発信クリックを2倍以上にできる可能性がある施策。 + +**具体的アクション**: +1. 計算ボタン押下後・結果表示時にのみ表示するCTAコンポーネントを実装 +2. コピー: 「この成績を毎試合記録しませんか? BUZZ BASEで自動管理 → App Storeで無料ダウンロード」 +3. モバイルとPC向けに表示を出し分け(モバイル: App Storeボタン / PC: QRコード) + +### 3位: Xアカウントの開設と初期投稿10件の作成・投稿 + +**理由**: 5月〜8月は野球シーズン最盛期でXの野球コミュニティが最も活発。今開始しなければ最大のSNS集客シーズンを逃す。本ドキュメントの投稿テンプレートを使えば初期10件は2〜3時間で作成可能。 + +**具体的アクション**: +1. @buzzbase_jp でXアカウント開設(プロフィール設定、App StoreリンクをBioに追記) +2. 本ドキュメントのテンプレート1〜5から5本を即時投稿 +3. 「#野球垢さんと繋がりたい」「#高校野球」でフォロー20〜30アカウント +4. 野球成績に関する投稿に有益なリプライを5〜10件送る + +--- + +## KPI設定と効果測定 + +### 月次KPI + +| 指標 | 4月実績(ベースライン) | 5月目標 | 6月目標 | 7月目標 | +|------|-------------------|--------|--------|--------| +| 月間セッション | 1,723 | 2,200 | 2,800 | 3,500 | +| App Store発信クリック | 85件 | 150件 | 200件 | 300件 | +| 推定月間DL | 13〜21件 | 25〜35件 | 40〜60件 | 60〜100件 | +| Xフォロワー | 0 | 50 | 100 | 200 | +| TikTokフォロワー | 0 | 100 | 300 | 600 | +| AI検索セッション | 4件 | 10件 | 20件 | 40件 | +| 対象クエリ平均CTR | 約1.0% | 1.8% | 2.5% | 3.0% | + +### 施策別計測方法 + +| 施策 | 計測指標 | 計測ツール | +|------|---------|----------| +| SEO title/description改善 | 対象クエリのCTR・クリック数 | Search Console(クエリ別フィルタ) | +| CTAバナー効果 | App Store発信クリック(ページ別) | GA4(イベント: outbound_click) | +| Xアカウント | セッション(utm_source=twitter)、フォロワー数 | GA4 + X Analytics | +| TikTok | セッション(utm_source=tiktok)、フォロワー数 | GA4 + TikTok Analytics | +| AI検索対応 | ai.google / chatgpt.com / perplexity.ai からのセッション | GA4(参照元別) | +| 全体DL | App Store発信クリック × 推定CVR(15〜25%) | GA4 + App Store Connect | + +### 週次チェックリスト(毎週月曜、30分) + +- [ ] Search Consoleで対象クエリのCTR推移を確認(前週比) +- [ ] GA4でApp Store発信クリック数を確認(ページ別) +- [ ] X・TikTokの投稿実施数・フォロワー数・エンゲージメント率を確認 +- [ ] AI検索セッション数を確認 +- [ ] 最もパフォーマンスの高かったSNS投稿の内容を記録(次週の参考に) + +--- + +*本ドキュメントは以下と連携して運用する。* +*- `docs/strategy/marketing/aso-and-app-growth-plan.md`(ASO・Web→App導線の詳細)* +*- `docs/strategy/marketing/growth-plan.md`(全体グロース戦略)* +*- `docs/strategy/mobile-app-sns-growth-202604.md`(SNS戦略詳細・TikTok動画テンプレート)* +*次回レビュー: 2026年5月末(1ヶ月後のSEO・SNS初動確認)* diff --git a/docs/strategy/measurement-plan-web-to-app-202604.md b/docs/strategy/measurement-plan-web-to-app-202604.md new file mode 100644 index 0000000..618edd8 --- /dev/null +++ b/docs/strategy/measurement-plan-web-to-app-202604.md @@ -0,0 +1,645 @@ +# Web→App 登録獲得 計測実装プラン + +作成日: 2026-04-30 +対象期間データ: 2026-04-01〜2026-04-29 +GA4 Property: 428100753 + +--- + +## 1. 現状の計測穴の特定 + +### 1-1. 現状で「見えていないこと」 + +GA4 Enhanced Measurement(標準自動計測)で取得できているのは以下だけ。 + +| 取得できているもの | 限界 | +| --- | --- | +| page_view, session_start, first_visit | どのCTAから飛んだか不明 | +| scroll (90%スクロール) | どのCTAがビューポートに入ったか不明 | +| outbound click(linkUrl) | cta_locationが区別できない。SmartBanner/CtaBanner/CalculatorResultが全部まとめて "outbound" | +| form_start(フォームクリック) | どのフォームか不明。signup/signinが区別できない | + +現状では以下のファネルが**完全に不可視**。 + +``` +ランディング + → どのCTAを見た (impression) + → どのCTAをクリックした (click with location) + → App Store到達 (proxy指標 = クリック数) + → アプリ登録 (App Store内のため計測不可) +``` + +### 1-2. 具体的な穴 + +#### 穴1: /tools/ops でのCTAクリックが1.2%なのに原因が特定できない + +- 計算実行後に表示される `CalculatorForm` 内の「アプリで成績を記録する」ボタン(`calculator_result` CTA)と、ページ末尾の `CtaBanner` のどちらがクリックされているか区別できない +- 計算を実行したユーザーが何人いるかも不明(`calculator_calculate` イベントなし) +- 計算未実行で直帰しているのか、計算後に離脱しているのかが不明 + +#### 穴2: /tools/k-bb でクリック0件 → CTAコンポーネント状況の確認 + +- コードを確認したところ `CalculatorPageContent` を使っており `CtaBanner` は配置されている +- しかし `CalculatorForm` 内の calculator_result CTAは `results.length > 0` の条件付きなので計算実行がゼロなら表示もゼロ +- k-bbページへの流入が計算実行に至っているか確認できない + +#### 穴3: SmartAppBanner の貢献度がゼロ + +- SmartAppBanner は全ページ上部に表示、月間約85クリックの大半がTOPページ(48クリック) +- SmartBanner経由クリックかCtaBanner経由クリックか区別できない +- dismissした比率も不明(7日間リセット設計だが効果測定できていない) + +#### 穴4: signup/signin フローの分断 + +- form_start が 161回発火しているが、どのフォームか不明 +- signupフォームのsubmitが成功したか(registration-confirmationへの遷移)が計測されていない +- Googleログイン経由のsignupは別フローだが区別できない + +#### 穴5: コンバージョン(key_event)が1件も設定されていない + +- GA4のキーイベント未設定のため、Googleの機械学習・オーディエンス・広告連携がすべて無効化されている + +--- + +## 2. 実装すべき GA4 カスタムイベント設計 + +### 2-1. イベント一覧 + +| イベント名 | トリガー | 優先度 | +| --- | --- | --- | +| `app_store_click` | App Store URLへの遷移クリック | 最高 | +| `cta_banner_view` | CTAバナーがビューポートに入った | 高 | +| `calculator_calculate` | 計算ボタン押下(結果表示) | 高 | +| `signup_start` | /signup ページ到達 または フォーム最初の入力 | 中 | +| `signup_complete` | registration-confirmationページ到達 | 最高 | +| `smart_banner_dismiss` | SmartAppBannerの✕ボタンクリック | 中 | + +### 2-2. イベントパラメータ設計 + +#### `app_store_click`(最重要) + +``` +event_name: "app_store_click" +parameters: + cta_location: "smart_banner" | "hero" | "calculator_result" | "cta_banner_top" | "cta_banner_bottom" | "signup_page" | "signin_page" + source_page: "/tools/ops" など pathname + calculator_stat: "ops" | "obp" | "k-bb" | null (toolsページのみ) + has_calculated: true | false (calculator_result のみ。計算後クリックかどうか) +``` + +`cta_location` の命名規則: +- `smart_banner`: ページ上部の固定バナー +- `hero`: TOPページのヒーローセクション内CTA(TopLoaderコンポーネント内) +- `calculator_result`: CalculatorForm の計算結果直後のボタン +- `cta_banner_top`: CalculatorPageContent の calculatorSlot 直後のCtaBanner(1個目) +- `cta_banner_bottom`: CalculatorPageContent の末尾のCtaBanner(2個目) +- `signup_page`: /signup ページ内のCtaBanner +- `signin_page`: /signin ページ内のCtaBanner + +#### `cta_banner_view` + +``` +event_name: "cta_banner_view" +parameters: + cta_location: (app_store_click と同じ値) + source_page: pathname +``` + +#### `calculator_calculate` + +``` +event_name: "calculator_calculate" +parameters: + calculator_stat: "ops" | "obp" | "k-bb" | "era" | "bb-9" | "k-9" など + source_page: pathname +``` + +#### `signup_complete` + +``` +event_name: "signup_complete" +parameters: + signup_method: "email" | "google" + source_page: 直前のreferer(signupへ来る前のページ) +``` + +#### `smart_banner_dismiss` + +``` +event_name: "smart_banner_dismiss" +parameters: + source_page: pathname +``` + +### 2-3. GA4 キーイベント(コンバージョン)登録 + +以下の2つを必ず登録する。残りは参考指標として活用。 + +| イベント | キーイベント登録 | 理由 | +| --- | --- | --- | +| `app_store_click` | 登録する(主KPI) | App Store到達の proxy指標 | +| `signup_complete` | 登録する(主KPI) | Webアカウント登録完了 | +| `calculator_calculate` | 登録しない(参考) | 中間指標。CVに直結しない | +| `cta_banner_view` | 登録しない(参考) | impression指標 | + +--- + +## 3. Next.js App Router での実装方法 + +### 3-1. 共通ユーティリティ関数の設計 + +Server Component優先方針に準拠しつつ、イベント発火はすべてクライアントサイドで行う。Server Componentはトラッキングに関与しない。 + +**ファイル: `front/app/utils/gtag.ts`** + +```typescript +// クライアントサイド専用("use client" は不要、呼び出し側がClient Component) +declare global { + interface Window { + gtag: (command: string, action: string, params?: Record<string, unknown>) => void; + } +} + +export type CtaLocation = + | "smart_banner" + | "hero" + | "calculator_result" + | "cta_banner_top" + | "cta_banner_bottom" + | "signup_page" + | "signin_page"; + +export type GtagEventParams = { + app_store_click: { + cta_location: CtaLocation; + source_page: string; + calculator_stat?: string; + has_calculated?: boolean; + }; + cta_banner_view: { + cta_location: CtaLocation; + source_page: string; + }; + calculator_calculate: { + calculator_stat: string; + source_page: string; + }; + signup_complete: { + signup_method: "email" | "google"; + }; + smart_banner_dismiss: { + source_page: string; + }; +}; + +export function trackEvent<K extends keyof GtagEventParams>( + eventName: K, + params: GtagEventParams[K], +): void { + if (typeof window === "undefined" || typeof window.gtag !== "function") return; + window.gtag("event", eventName, params as Record<string, unknown>); +} +``` + +この `trackEvent` は型安全にイベント名とパラメータをペアで管理できる。呼び出し側で `import { trackEvent } from "@app/utils/gtag"` するだけで使える。 + +### 3-2. SmartAppBanner の改修 + +SmartAppBanner はすでに `"use client"` のため、onClick を追加するだけでよい。`usePathname` を使って `source_page` を取得する。 + +**改修箇所: `front/app/(app)/_components/SmartAppBanner.tsx`** + +```typescript +"use client"; + +import { usePathname } from "next/navigation"; +import { trackEvent } from "@app/utils/gtag"; +// ... 既存 import + +export default function SmartAppBanner() { + const pathname = usePathname(); + // ... 既存 state/ref + + const handleDismiss = () => { + localStorage.setItem(STORAGE_KEY, String(Date.now())); + setVisible(false); + trackEvent("smart_banner_dismiss", { source_page: pathname }); + }; + + // App Store リンクの onClick + const handleAppStoreClick = () => { + trackEvent("app_store_click", { + cta_location: "smart_banner", + source_page: pathname, + }); + }; + + // ... 既存の表示ロジック + + return ( + <div ...> + ... + <a + href={APP_STORE_URL} + target="_blank" + rel="noopener noreferrer" + onClick={handleAppStoreClick} // 追加 + className="shrink-0" + > + ... + </a> + </div> + ); +} +``` + +### 3-3. CtaBanner の改修方針(Server Component を保持しつつ計測する) + +CtaBanner は現在 Server Component。App Storeリンクに onClick を付けるためだけに全体を Client Component にするのは避ける。以下のパターンを採用する。 + +**パターン: リンク部分のみを薄い Client Component に切り出す** + +**新規ファイル: `front/app/(app)/_components/AppStoreLinkButton.tsx`** + +```typescript +"use client"; + +import Image from "next/image"; +import { usePathname } from "next/navigation"; +import { trackEvent, type CtaLocation } from "@app/utils/gtag"; +import { APP_STORE_URL } from "@app/constants/app"; + +type Props = { + ctaLocation: CtaLocation; +}; + +export default function AppStoreLinkButton({ ctaLocation }: Props) { + const pathname = usePathname(); + + const handleClick = () => { + trackEvent("app_store_click", { + cta_location: ctaLocation, + source_page: pathname, + }); + }; + + return ( + <a + href={APP_STORE_URL} + target="_blank" + rel="noopener noreferrer" + className="inline-block" + onClick={handleClick} + > + <Image + src="/images/download_app_store_badge_jp.svg" + alt="App Storeからダウンロード" + width={150} + height={50} + className="h-[44px] w-auto" + /> + </a> + ); +} +``` + +**改修: `front/app/(app)/_components/CtaBanner.tsx`** + +```typescript +import Image from "next/image"; +import AppStoreLinkButton from "./AppStoreLinkButton"; +import type { CtaLocation } from "@app/utils/gtag"; + +type Props = { + heading?: string; + body: string; + className?: string; + ctaLocation?: CtaLocation; // 追加 +}; + +export default function CtaBanner({ + heading, + body, + className = "mt-10", + ctaLocation = "cta_banner_top", // デフォルト +}: Props) { + return ( + <section className={...}> + ... + <AppStoreLinkButton ctaLocation={ctaLocation} /> + ... + </section> + ); +} +``` + +**CtaBanner 呼び出し側の変更(CalculatorPageContent.tsx):** + +```typescript +// 1個目のCtaBanner(calculatorSlot直後) +<CtaBanner + heading={...} + body={...} + ctaLocation="cta_banner_top" +/> + +// 2個目のCtaBanner(ページ末尾) +<CtaBanner + heading="チームの成績をアプリでまとめて管理" + body="..." + ctaLocation="cta_banner_bottom" +/> +``` + +signup/signin ページ内の CtaBanner: + +```typescript +// signup/page.tsx +<CtaBanner + body="アプリならもっと便利に成績を記録・管理できます。" + ctaLocation="signup_page" +/> + +// signin/page.tsx +<CtaBanner + body="アプリならもっと便利に成績を記録・管理できます。" + ctaLocation="signin_page" +/> +``` + +### 3-4. CalculatorForm の改修 + +`CalculatorForm` はすでに `"use client"` なので、`usePathname` と `trackEvent` を直接呼ぶ。 + +**改修箇所: `front/app/(app)/tools/_components/CalculatorForm.tsx`** + +```typescript +"use client"; + +import { usePathname } from "next/navigation"; +import { trackEvent } from "@app/utils/gtag"; +// ... 既存 import + +type Props = { + fields: CalculatorField[]; + outputs: CalculatorOutput[]; + calculate: (...) => ...; + nextActions?: NextAction[]; + calculatorStat?: string; // 追加: "ops" | "obp" | "k-bb" など +}; + +export default function CalculatorForm({ fields, outputs, calculate, nextActions, calculatorStat }: Props) { + const pathname = usePathname(); + // ... 既存 state + + const handleCalculate = useCallback(() => { + // ... 既存ロジック(計算処理) + + // 計算成功時にイベント発火 + if (calculatorStat) { + trackEvent("calculator_calculate", { + calculator_stat: calculatorStat, + source_page: pathname, + }); + } + }, [values, fields, outputs, calculate, calculatorStat, pathname]); + + const handleAppStoreClick = () => { + trackEvent("app_store_click", { + cta_location: "calculator_result", + source_page: pathname, + calculator_stat: calculatorStat, + has_calculated: true, + }); + }; + + return ( + ... + {results.length > 0 ? ( + <div className="mt-4 text-center"> + <a + href={APP_STORE_URL} + target="_blank" + rel="noopener noreferrer" + onClick={handleAppStoreClick} // 追加 + className="inline-block w-full ..." + > + アプリで成績を記録する(無料) + </a> + </div> + ) : null} + ... + ); +} +``` + +**各Calculator コンポーネントの変更(例: OpsCalculator.tsx):** + +```typescript +export default function OpsCalculator() { + return ( + <CalculatorForm + fields={definition.fields} + outputs={definition.outputs} + calculate={definition.calculate} + nextActions={nextActions} + calculatorStat="ops" // 追加 + /> + ); +} +``` + +### 3-5. signup_complete の計測 + +`SignUp.tsx` の `handleSubmit` は成功時に `router.push("/registration-confirmation")` している。この直前にイベント発火を挿入する。 + +**改修箇所: `front/app/components/auth/SignUp.tsx`** + +```typescript +import { trackEvent } from "@app/utils/gtag"; + +const handleSubmit = async (event: React.FormEvent) => { + // ... 既存ロジック + try { + await signUp({ ... }); + trackEvent("signup_complete", { signup_method: "email" }); // 追加 + router.push("/registration-confirmation"); + } catch ... +}; +``` + +Googleログイン経由のsignupは `GoogleLoginButton` コンポーネント内に同様の発火処理を追加する。 + +### 3-6. GTM導入の判断 + +**結論: GTM は導入しない。** + +理由: +- 現状 `layout.tsx` に gtag.js を直書きしており、カスタムイベントの追加も `window.gtag()` 呼び出しで完結する +- GTMを追加するとスクリプトが2重になるリスクがある +- 個人開発の工数制約で、GTMのコンテナ設定・公開フローを維持するコストは不要 +- 今回の計測要件はすべて `trackEvent()` util と各コンポーネントの onClick で実装できる + +--- + +## 4. GA4 側の設定 + +### 4-1. キーイベント(コンバージョン)登録手順 + +GA4管理画面 > 管理 > イベント > カスタムイベントを作成後、以下を「キーイベントとしてマーク」: + +1. `app_store_click`(主KPI: App Store到達 proxy) +2. `signup_complete`(主KPI: Webアカウント登録完了) + +### 4-2. カスタムディメンション登録 + +GA4管理画面 > 管理 > カスタム定義 > カスタムディメンション で以下を登録する。これをしないとイベントパラメータをレポートで絞り込めない。 + +| ディメンション名 | スコープ | イベントパラメータ名 | +| --- | --- | --- | +| CTA設置場所 | イベント | cta_location | +| 計算指標 | イベント | calculator_stat | +| サインアップ方法 | イベント | signup_method | + +### 4-3. Looker Studio / GA4 探索レポートで構築するファネル + +#### Web→App 登録ファネル(探索レポート > ファネルデータ探索) + +``` +ステップ1: セッション開始(session_start) +ステップ2: CTAバナー表示(cta_banner_view) ← impression率 +ステップ3: App Storeクリック(app_store_click) ← CTA CTR +ステップ4: App Store登録(計測不可、別途Apple Search Ads Console参照) +``` + +このファネルを cta_location ディメンションでセグメント分割すると「どのCTAが最もApp Store送客に貢献しているか」が可視化できる。 + +#### 計算ツール→App Store クリック率レポート(GA4 探索 > 自由形式) + +- ディメンション: source_page, calculator_stat, cta_location +- 指標: app_store_click(イベント数), calculator_calculate(イベント数) +- 計算フィールド: app_store_click / calculator_calculate = 計算後クリック率 + +#### Looker Studio 構築定義(Web→App 登録パフォーマンスダッシュボード) + +``` +ブロック1: KPIカード + - 月間 app_store_click 数(前月比) + - 月間 signup_complete 数(前月比) + - ページ別 App Store CTR(app_store_click / sessions) + +ブロック2: CTA別パフォーマンス(棒グラフ) + - ディメンション: cta_location + - 指標: app_store_click数, session数, CTR + +ブロック3: ツールページ別 計算→クリックファネル(テーブル) + - source_page, calculator_calculate, app_store_click, クリック率 + +ブロック4: 流入元別 App Store クリック(パイチャート) + - ディメンション: セッションのデフォルトチャネルグループ + - 指標: app_store_click数 +``` + +--- + +## 5. Apple Search Ads / `ct=` パラメータの活用案 + +App Storeクリックの正確なアトリビューションは AppsFlyer/Adjust なしでは困難だが、以下2点は無料で対応できる。 + +### 5-1. `ct=` キャンペーンパラメータ + +App Store URL に `ct=` パラメータを付与することで、Apple Search Ads Console および App Store Connect Analytics でキャンペーン別クリック数・インストール数が分かる。 + +```typescript +// app/constants/app.ts を改修 + +export const APP_STORE_BASE_URL = "https://apps.apple.com/jp/app/buzz-base/id6761011816"; + +export const APP_STORE_URLS = { + smart_banner: `${APP_STORE_BASE_URL}?ct=web_smart_banner`, + hero: `${APP_STORE_BASE_URL}?ct=web_hero`, + calculator_result: `${APP_STORE_BASE_URL}?ct=web_calc_result`, + cta_banner_top: `${APP_STORE_BASE_URL}?ct=web_cta_top`, + cta_banner_bottom: `${APP_STORE_BASE_URL}?ct=web_cta_bottom`, + signup_page: `${APP_STORE_BASE_URL}?ct=web_signup`, + signin_page: `${APP_STORE_BASE_URL}?ct=web_signin`, +} as const; + +export type CtaLocationKey = keyof typeof APP_STORE_URLS; +``` + +`AppStoreLinkButton.tsx` と各コンポーネントで `APP_STORE_URL` の代わりに `APP_STORE_URLS[ctaLocation]` を使う。App Store Connect Analytics で「キャンペーン」列を確認するとCTA別のインストール数が分かるようになる。 + +### 5-2. Apple Search Ads Attribution API + +Apple Search Ads を出稿しない場合でも Attribution API(`iAd` フレームワーク後継)はアプリ初回起動時のオーガニック流入元を返す。現在の iOS アプリでこの API を叩いてサーバーに送信すれば、「どのページからApp Storeを開いてインストールしたか」を間接的に追えるが、実装工数が大きいため今フェーズでは非推奨。`ct=` パラメータのみで十分な精度が得られる。 + +--- + +## 6. 計測実装後に実行する初回分析プラン + +計測が2週間程度まわったタイミング(実装後 2026-05-14 前後)で以下を確認する。 + +### 仮説1: /tools/ops の App Store CTR 1.2% は「計算未実行ユーザーが多い」ことが原因 + +- 確認方法: `calculator_calculate` 数 / sessions on /tools/ops = 計算実行率 +- 期待値: 計算実行ユーザーの CTR が 10% 超なら「流入後に計算しないで離脱」が問題 +- アクション: ページ上部にフォームを移動する、またはデフォルト値を入れておく + +### 仮説2: SmartAppBanner は dismiss 率が高く、実質的に邪魔なだけになっている + +- 確認方法: `smart_banner_dismiss` 数 / `cta_banner_view`(smart_banner) 数 = dismiss 率 +- 期待値: dismiss率が 60% 超なら7日間 → 14日間に延ばすか、表示タイミングを変更 +- 対比: SmartBanner CTR(app_store_click with cta_location=smart_banner / views) + +### 仮説3: /tools/k-bb の CTR 0 は計算実行率が低いことで説明できる + +- 確認方法: `calculator_calculate` where calculator_stat = "k-bb" の数 +- 期待値: 計算実行率が ops と同等なら CTA の問題、低ければフォーム UI の問題 + +### 仮説4: cta_location 別 CTR には 3倍以上の差がある + +- 確認方法: `app_store_click` を cta_location でブレイクダウン +- 期待値: calculator_result が最高 CTR(計算直後は動機が最も高い) +- アクション: CTR が最も高い CTA のデザインを他の CTA にも適用する + +### 仮説5: オーガニック流入ユーザーの signup_complete 率は 1% 未満 + +- 確認方法: sessions(google/organic)に対する signup_complete 数の比率 +- 期待値: 1% 未満なら「ツールのみ使って離脱」が常態化している証拠 +- アクション: 計算結果に「この成績を保存するにはアプリ登録が必要」のコピー追加 + +--- + +## 実装工数見積もり(優先順位順) + +| 実装項目 | 工数 | 優先度 | +| --- | --- | --- | +| `gtag.ts` util 作成 | 30分 | 最高 | +| `AppStoreLinkButton.tsx` 作成 | 30分 | 最高 | +| `CtaBanner.tsx` 改修(AppStoreLinkButton組み込み) | 30分 | 最高 | +| `SmartAppBanner.tsx` 改修(onClick追加) | 15分 | 最高 | +| `CalculatorForm.tsx` 改修(trackEvent追加、calculatorStat prop追加) | 30分 | 最高 | +| 各Calculator(Ops/Obp/KBB等)に calculatorStat prop 追加 | 30分 | 高 | +| `SignUp.tsx` に signup_complete 発火追加 | 15分 | 高 | +| GA4 カスタムディメンション登録(管理画面操作) | 15分 | 高 | +| GA4 キーイベント登録(管理画面操作) | 10分 | 高 | +| `app/constants/app.ts` に ct= パラメータ対応URL追加 | 20分 | 中 | +| GA4 探索レポート / Looker Studio 構築 | 60分 | 中 | + +**合計: 約4〜5時間(1日以内に完結)** + +--- + +## まとめ: 最小実装セット(今日やること) + +1. `front/app/utils/gtag.ts` を新規作成 +2. `AppStoreLinkButton.tsx` を新規作成(`"use client"` の薄いラッパー) +3. `CtaBanner.tsx` を改修(AppStoreLinkButton + ctaLocation prop) +4. `SmartAppBanner.tsx` を改修(usePathname + onClick + trackEvent) +5. `CalculatorForm.tsx` を改修(handleCalculate内にtrackEvent追加、アプリリンクにonClick追加) +6. `OpsCalculator.tsx` / `KBBCalculator.tsx` / `ObpCalculator.tsx` に `calculatorStat` prop 追加 +7. `SignUp.tsx` に `signup_complete` 発火追加 +8. GA4管理画面でキーイベント2件登録・カスタムディメンション3件登録 + +これだけで「どのCTAが何回クリックされたか」「どのツールページで計算が実行されたか」「何人がWebアカウント登録を完了したか」がすべてGA4で可視化できるようになる。 diff --git a/docs/strategy/mobile-app-sns-growth-202604.md b/docs/strategy/mobile-app-sns-growth-202604.md new file mode 100644 index 0000000..cd1c9be --- /dev/null +++ b/docs/strategy/mobile-app-sns-growth-202604.md @@ -0,0 +1,447 @@ +# モバイルアプリ ユーザー獲得 × SNS戦略 統合レポート + +**作成日**: 2026年4月6日 +**分析テーマ**: モバイルアプリのユーザー数増加(SNS活用含む) +**分析体制**: 6エージェント並列分析(戦略統括・グロース・市場調査・プロダクト企画・広告最適化・データ分析) + +--- + +## エグゼクティブサマリー + +### 全エージェント共通の最重要認識 + +**「競合はSNSを全くやっていない。ここはブルーオーシャン。」** + +ベボレコ・ヒットメーカー・TeamHubのいずれもSNS運用がほぼゼロ。BUZZ BASEが今すぐSNSに参入すれば、野球成績アプリカテゴリで唯一のSNS発信者になれる。 + +### 全エージェントが共通して最優先と判断した施策トップ5 + +| 優先度 | 施策 | 工数 | 期待効果 | +|--------|------|------|---------| +| S | 成績カード画像生成&SNSシェア機能 | 3〜4日 | バイラル成長の核。UGC促進 | +| S | グループ招待URLの強化(コード→URL方式) | 2〜3日 | チーム単位DLの起爆剤 | +| S | **TikTokをメインSNSとして本格運用** | 週3〜5本 | フォロワー0でもバズれる唯一のSNS。中学生リーチ最強 | +| A | X公式アカウント運用 | 継続(毎日15分) | 野球垢文化との接続 | +| A | 計算ツール結果直後のアプリCTA追加(Web) | 半日 | 既存トラフィックの転換率向上 | + +### 3ヶ月後の目標 + +| 指標 | 現状 | 3ヶ月後目標 | +|------|------|------------| +| 月間アプリDL | 推定34 | 150 | +| アプリDAU | 未計測 | 20 | +| TikTokフォロワー | 0 | 1,000 | +| Xフォロワー | 0 | 200 | +| Instagramフォロワー | 0 | 200 | + +--- + +## 1. 現状分析 + +### トラフィック(データ分析エージェント) +- 月間アクティブユーザー: 838人(96%が新規) +- 流入: Google organic 58%、Yahoo 22%、Direct 14%、SNS 0.7% +- 計算ツール系がSEO流入の主力(OPS計算: 月10,877表示) +- **デバイス: 78.5%がモバイル**(ターゲットの中高生と一致) + +### 最大の課題: 4つの「断絶」(戦略統括エージェント) +1. **導線の不在**: Web→アプリへの誘導の仕組みが存在しない +2. **認知の不在**: SNSプレゼンスがゼロ。SEO以外の発見機会がない +3. **口コミの不在**: 成績シェア機能がなく、ユーザーが自発的に広める仕組みがない +4. **ストアでの発見性**: レビュー0件、ASOが未最適化 + +### ユーザー獲得ファネルのボトルネック(データ分析エージェント) + +``` +Search Console 表示: 18,848回 + ↓ CTR: 3.16% +クリック: 591(73日間) + ↓ 登録率: 約19% +会員登録: 月170人 + ↓ DL転換: 極めて低い +アプリDL: 推定34/月 +``` + +**最大ボトルネック**: 高表示・0クリッククエリの多さ(「野球 失点とは」1位表示なのに0クリック等) + +### 競合環境(市場調査エージェント) +- ベボレコ(4,323レビュー)、TeamHub(4,377レビュー)、ヒットメーカー(1,809レビュー) +- **全競合がSNS運用ほぼゼロ** → BUZZ BASEの先行チャンス +- 中高生野球人口: 約30〜35万人、現在浸透率0.3% + +--- + +## 2. SNSプラットフォーム別戦略 + +### 優先順位(グロース + 市場調査 + 戦略統括の総合判断) + +| 優先度 | プラットフォーム | 理由 | +|--------|----------------|------| +| **S** | **TikTok(メイン)** | フォロワー0でもバズれる唯一のSNS。中学生利用率が非常に高い。1投稿あたりシェア数がInstagramの4倍超。野球関連コンテンツが活発に流通中 | +| **A** | **X (Twitter)** | 野球垢文化の存在、テキスト中心で運用負荷最低、既に月10人流入実績あり | +| **A** | **Instagram** | 高校生利用率最高水準。TikTok動画をリールに転用(コンテンツリパーパシング) | +| **B** | **LINE** | 集客チャネルではなく「チーム内拡散の導線」として活用 | +| **C** | **YouTube** | 工数に対するリターンが小さい。余裕があれば | + +### TikTok 戦略(メインSNS) + +**なぜTikTokをメインにするか** +- フォロワー0でもアルゴリズムがコンテンツを評価して拡散してくれる(他SNSにはない特性) +- 中学生のSNS利用率でLINEの次に高い(ターゲット層に直撃) +- 1投稿あたりシェア数170回(Instagramの41回の4倍超) +- 「高校野球」「野球部」関連コンテンツが既に活発に流通 +- フォトモード(静止画カルーセル)活用で動画制作コストを大幅に下げられる +- 競合は誰もTikTokをやっていない(完全なブルーオーシャン) + +**コンテンツ設計(週3〜5本)** + +| タイプ | 具体例 | 効果 | 制作時間 | +|--------|--------|------|---------| +| 成績デモ | 「俺の打率入れたらグループ1位だった」30秒動画 | アプリ価値を即伝達 | 15分 | +| 野球部あるある | 「成績気にしすぎる野球部員の日常」 | エンタメでリーチ拡大 | 20分 | +| チャレンジ系 | 「チームで一番OPS高いの誰?コメントで教えて」 | コメント・シェア促進 | 15分 | +| 甲子園連動 | 「大谷翔平の高校時代の成績入れたら何位?」 | 8月に最大バズ | 30分 | +| フォトモード | 成績カード画像のカルーセル投稿 | 低コスト高シェア率 | 10分 | +| クイズ系 | 「野球の指標わかる?OPSとは」 | バズりやすい形式 | 15分 | +| 大会前後 | 「夏の大会前あるある」「最後の夏の成績を記録」 | 感情に訴える | 20分 | + +**投稿スケジュール** + +| 曜日 | コンテンツ | 備考 | +|------|-----------|------| +| 月 | 成績デモ / アプリ紹介系 | 週始めに実用的コンテンツ | +| 水 | 野球あるある / エンタメ系 | バイラル狙い | +| 金 | チャレンジ / クイズ系 | 週末の試合前にエンゲージメント | +| 土(任意) | フォトモード(成績カード) | 低コストで追加投稿 | +| 日(任意) | 大会連動 / トレンド便乗 | 試合結果が出るタイミング | + +**最適投稿時間**: 平日20:00〜22:00(練習後・帰宅後)、土日15:00〜17:00(試合後) + +**TikTokバズの鉄則** +1. 最初の1秒で視聴者を掴む(「チームで一番打ってるの誰?」等の問いかけ) +2. 30秒以内に収める(完走率がアルゴリズムに直結) +3. コメントを促す仕掛け(「コメントで教えて」「あなたのOPSは?」) +4. トレンドBGMを使う(野球関連の人気音源をチェック) +5. テキストオーバーレイ必須(音なしでも内容が伝わるように) + +**フォロワー獲得の初速戦略** +1. アカウント開設後、最初の1週間で動画10本を集中投下(アルゴリズムに評価されやすい) +2. 野球関連のトレンドハッシュタグを毎日チェックして便乗 +3. 他の野球系TikTokerの動画にコメント・デュエット +4. 初期は量を重視(完成度より投稿頻度) + +**動画スクリプト例** + +案1: 成績あるある(30秒) +``` +「野球部員の成績の気にし方あるある +①試合終わってすぐ打率計算する +②チームメイトの打率を勝手に計算する +③監督に言われる前に自分の成績を把握している +→ BUZZBASEで全部管理できます(アプリ画面見せる)」 +``` + +案2: チーム内ランキング(30秒) +``` +「野球部にいる成績にうるさい人あるある +①「俺の打率何割?」が口癖 +②チームの成績を全部暗記してる +③ランキングが気になって夜眠れない +→ そんな君のためのアプリ BUZZBASE」 +``` + +案3: 大会シーズン(30秒) +``` +「夏の大会前あるある +①突然練習が厳しくなる +②成績を残したくて焦る +③でも成績をちゃんと記録してない +→ 今から始めれば夏の大会の成績全部残せます」 +``` + +### X (Twitter) 戦略(サブSNS) + +TikTokがメインだが、Xも並行運用する。野球垢文化との接続に最適。 + +**投稿ローテーション(週3〜5投稿、毎日10分)** + +| カテゴリ | 具体例 | 頻度 | +|---------|--------|------| +| 成績Tips | 「OPSを上げる3つの考え方」 | 週1 | +| TikTok動画の告知 | 「TikTokで新しい動画出しました」 | 週2〜3 | +| 野球コミュニティ交流 | リプライ・引用RT | 毎日 | + +**ハッシュタグ**: `#BUZZBASE` `#野球` `#野球部` `#高校野球` `#中学野球` + +### Instagram 戦略(TikTokコンテンツの転用先) + +| フォーマット | 内容 | 頻度 | +|------------|------|------| +| リール | TikTok動画をそのまま転用 | 週2〜3回 | +| ストーリーズ | ユーザーの成績カードリポスト | 週2回 | +| フィード | 成績カード画像 | 週1回 | + +**ポイント**: Instagramは独自コンテンツを作らず、TikTok動画のリパーパシング(再利用)で運用コストを最小化する。 + +--- + +## 3. プロダクト施策(SNS連携機能) + +### 3-1. 成績カード画像生成&SNSシェア機能(プロダクト企画エージェント) + +**成績カードのデザイン仕様(1080×1080px)** +``` +┌──────────────────────────────────────┐ +│ BUZZ BASE ロゴ(左上) 日付・大会名(右上)│ +├──────────────────────────────────────┤ +│ vs 〇〇高校 5 - 3 勝 │ +├──────────┬───────────────────────────┤ +│ 打撃成績 │ 3打数2安打 1本塁打 3打点 │ +│ 投球成績 │ 5回2/3 2失点 7K ERA2.18│ +├──────────────────────────────────────┤ +│ ユーザー名(@handle) チーム名 │ +│ #BUZZBASE #高校野球 #野球垢 │ +└──────────────────────────────────────┘ +``` + +画像サイズ: Instagram正方形1080×1080、Xタイムライン1200×630、Stories1080×1920 + +**シェアテキストテンプレート** +``` +【試合後】 +{日付} vs {対戦相手} +打撃: {打数}打数{安打}安打 {打点}打点 +#BUZZBASE #高校野球 + +【好成績時】 +{安打数}安打 {打点}打点の活躍!今日のOPS: {OPS値} +📱 BUZZ BASEで毎試合の成績を記録中 +#BUZZBASE #野球垢 +``` + +### 3-2. グループ招待URL強化(プロダクト企画エージェント) + +現状: 招待コードをテキストで貼り付ける方式(手入力が必要で離脱ポイント) + +**改善後のUXフロー**: +``` +【送る側】 +グループ詳細 → 「メンバーを招待」→ buzzbase.jp/join/{code} のURL生成 → LINEでシェア + +【受け取る側(アプリあり)】 +URLタップ → Universal Links → アプリ起動 → グループ参加確認 → 完了 + +【受け取る側(アプリなし)】 +URLタップ → Webのグループ紹介ページ → App Storeボタン → DL → サインアップ → 自動参加 +``` + +**OGPタグ**: `og:title: "{グループ名}のランキングに参加しませんか?"` + +### 3-3. バイラルループ設計 + +``` +1人が登録・成績入力 + ↓ +ランキングを使いたい → チームメイトを招待(LINEグループへワンタップ共有) + ↓ +チームメイトが登録 → グループ内で広がる + ↓ +「うちのチームで始めよう」が生まれる + ↓ +チーム単位でのDL爆発(10〜20人/チーム) +``` + +### 3-4. エンゲージメント機能 + +- **週次成績サマリー通知**: 毎週月曜8:00配信(「先週の成績: 打率.350、ランキング3位」) +- **週間MVP機能**: グループ内で最も活躍したユーザーを自動選出、ゴールド背景のMVPカード生成 +- **招待バッジ**: 初招待「先輩選手」、3人招待「チームの柱」、5人招待「ムードメーカー」 + +--- + +## 4. コンテンツ戦略 + +### 中高生に刺さるテーマ15選(グロースエージェント) + +| # | テーマ | 形式 | 優先度 | +|---|--------|------|-------| +| 1 | 「俺の成績ランキング〇位だった」自慢系 | TikTok/リール | S | +| 2 | 野球部員あるある(成績・練習編) | TikTok/リール | S | +| 3 | 「チームで一番○○なのは誰?」チャレンジ | TikTok/X | S | +| 4 | 成績カード紹介(UGC活用) | Instagram/X | A | +| 5 | OPS・打率の計算方法解説 | TikTok/リール | A | +| 6 | 「大会前の目標成績を入力してみた」 | TikTok/リール | A | +| 7 | 夏の甲子園予選シーズン速報的投稿 | X | A | +| 8 | 「甲子園選手の成績をBUZZBASEで管理したら」 | TikTok/リール | A | +| 9 | 成績の伸びグラフ(Before/After) | Instagram/リール | B | +| 10 | 「成績管理アプリを使ってみた」使い方動画 | TikTok/YouTube | B | + +### UGC促進の仕組み + +- 公式ハッシュタグ `#BUZZBASE` で投稿された成績カードを公式アカウントでリポスト +- 月間MVP発表: 月末にランキング1位ユーザーを公式SNSで紹介 +- 成績カード画像にBUZZBASEロゴ・URLを自動埋め込み(広告感を出しすぎない) + +--- + +## 5. 季節性の活用 + +| 時期 | イベント | SNSテーマ | +|-----|---------|---------| +| **4月(今)** | 春季大会、新入部員入部 | 「新チームで成績比較しよう」 | +| 5月 | 春季地区大会 | 「春季大会成績記録しよう」 | +| 6月 | 夏の大会予選抽選 | 「夏に向けて成績を管理しよう」 | +| **7月(重要)** | 夏の大会予選 | 「最後の夏の成績を記録」感情的コンテンツ | +| **8月(最重要)** | 甲子園、中学新チーム始動 | 甲子園連動コンテンツで最大リーチ | +| 9月 | 秋季大会、高校新チーム始動 | 「新チーム登録キャンペーン」 | +| 11〜3月 | オフシーズン | 年間成績サマリーのシェア | + +--- + +## 6. 収益シミュレーション(広告最適化エージェント) + +### アプリ内広告の段階的導入 + +| フェーズ | 条件 | フォーマット | +|---------|------|------------| +| 1 | AdMob導入直後 | バナーのみ(ダッシュボード・試合一覧フッター) | +| 2 | DAU 50以上 + レビュー10件以上 | + インタースティシャル(試合記録保存後・1日1回上限) | +| 3 | DAU 100以上 | + リワード動画(過去シーズン閲覧等) | + +### DAU別 月間収益予測(Web + アプリ統合) + +| DAU | アプリ収益 | Web収益 | 統合月間収益 | +|-----|---------|---------|------------| +| 25 | 299円 | 860円 | 1,159円 | +| 50 | 596円 | 880円 | 1,476円 | +| 100 | 1,193円 | 920円 | 2,113円 | +| 250 | 2,981円 | 1,040円 | 4,021円 | +| 500 | 5,963円 | 1,240円 | 7,203円 | + +**月収1万円の最速ルート**: アプリDAU 250 + Web PV 20,000 + アフィリエイト記事3本 ≈ 約10,000〜12,000円 + +--- + +## 7. 測定・トラッキング設計(データ分析エージェント) + +### UTMパラメータ設計 + +``` +X公式投稿: +https://buzzbase.jp/tools/ops?utm_source=twitter&utm_medium=social&utm_campaign=ops_tool_promo + +選手のシェア: +https://buzzbase.jp/mypage/{userid}?utm_source=twitter&utm_medium=social&utm_campaign=player_share + +TikTok: +https://buzzbase.jp/?utm_source=tiktok&utm_medium=social&utm_campaign=tiktok_promo +``` + +### 週次KPIダッシュボード + +| KPI | 現状 | 目標 | 計測方法 | +|-----|------|------|---------| +| 週間アクティブユーザー | 約86人/週 | 250人/週 | GA4 | +| 週間新規会員登録 | 約25人/週 | 50人/週 | GA4 | +| SNS起因セッション | 約1.4/週 | 50/週 | GA4 (utm_medium=social) | +| 週間アプリDL | 約8/週 | 35/週 | App Store Connect | + +--- + +## 8. 3ヶ月ロードマップ + +### 4月: 基盤構築 + SNS立ち上げ(目標DL: 60) + +| 週 | 施策 | 工数 | +|----|------|------| +| 第1週 | 計算ツールCTA追加(Web) | 半日 | +| 第1週 | X公式アカウント開設 + 初期投稿5件 | 3時間 | +| 第1週 | TikTokアカウント開設 + 第1投稿 | 2〜3時間 | +| 第2週 | App Storeメタデータ最適化 | 1.5時間 | +| 第2週 | Instagram公式アカウント開設 | 1時間 | +| 第2週 | SNS投稿の定常運用開始(X毎日、TikTok週2-3) | 継続 | +| 第3週 | 成績テキストシェア機能の改善 | 1日 | +| 第4週 | レビュー依頼ダイアログ実装 | 半日 | + +### 5月: バイラル機能 + SNS本格化(目標DL: 100) + +| 週 | 施策 | 工数 | +|----|------|------| +| 第1-2週 | 成績カード画像生成&SNSシェア機能 | 3〜4日 | +| 第2週 | SEO記事(OPSとは、打率の計算方法等) | 1日 | +| 第3週 | グループ招待URL設計着手 | 設計1日 | +| 全週 | TikTok・Instagram定常運用、UGCリポスト | 週5-8時間 | + +### 6月: 拡大 + 夏の大会準備(目標DL: 150) + +| 週 | 施策 | 工数 | +|----|------|------| +| 第1-2週 | グループ招待URL機能実装 | 3〜5日 | +| 第2週 | 週次成績サマリー通知実装 | 2日 | +| 第3週 | 「夏の大会に向けて」キャンペーン開始 | 2時間 | +| 第4週 | 3ヶ月PDCAレビュー、7月戦略調整 | 2時間 | + +--- + +## 9. GitHub Issue案(優先度順) + +| # | Issue | 優先度 | 工数 | 対象 | +|---|-------|--------|------|------| +| A | 成績カード画像生成&SNSシェア機能 | P0 | 3〜4日 | mobile | +| B | グループ招待URLの実装(コード→URL方式) | P0 | 2〜3日 | mobile+back+front | +| C | 計算ツール結果連動CTAの実装 | P0 | 半日 | front | +| D | 週次成績サマリー プッシュ通知 | P1 | 2日 | mobile+back | +| E | 週間MVP機能&MVPカード生成 | P1 | 3〜4日 | mobile+back | + +--- + +## 10. PDCAサイクル + +### 週次(毎週月曜、30分) +- SNS投稿の実施数(計画 vs 実績) +- フォロワー増加数(週あたり+15人以上か) +- 最もエンゲージメントの高かった投稿の分析 +- アプリDL数 + +### 月次(毎月最終日、1時間) +- 全KPIの目標 vs 実績 +- チャネル別ROI比較 +- 効果の低いチャネルは一時停止を検討 + +### 判断基準 +- DL目標の120%以上達成 → 翌月目標を上方修正 +- DL目標の80%未満 → 施策優先順位を再検討。効果の低いチャネルは一時停止 +- リテンション率15%未満 → アプリUX改善を最優先に切り替え + +--- + +## 個人開発のリソース管理 + +### 週あたりの時間配分(合計5〜7時間) + +| 活動 | 時間/週 | +|------|--------| +| TikTok動画の制作・投稿(メイン) | 2.5〜3.5時間 | +| X投稿・コミュニティ交流 | 1時間 | +| Instagram転用投稿 | 30分 | +| 計測・分析 | 30分 | + +### 明示的に「やらないこと」 +- YouTube動画の定期制作 +- LINE公式アカウント運営 +- 有料広告の出稿(MAU 1,000超まで) +- Instagram独自コンテンツの制作(TikTokからの転用のみ) + +### 燃え尽き防止 +- 毎月第4週はSNS投稿を減らし、振り返りに充てる +- 投稿の「型」を5パターン作り、数値と画像の差し替えで量産 +- 週末に翌週の投稿を5本まとめて作成し予約投稿 + +--- + +## 関連ドキュメント + +- [前回統合レポート(4/2)](ios-app-download-growth-integrated-202604.md) +- [SEO成長計画](seo-growth-plan-202604.md) +- [収益化戦略](monetization-strategy-ios-web-202604.md) +- [競合分析](research/market-research-report.md) diff --git a/docs/strategy/monetization-strategy-ios-web-202604.md b/docs/strategy/monetization-strategy-ios-web-202604.md new file mode 100644 index 0000000..72ff289 --- /dev/null +++ b/docs/strategy/monetization-strategy-ios-web-202604.md @@ -0,0 +1,396 @@ +# BUZZ BASE 収益化統合戦略 - iOSアプリ + Web広告(2026年4月) + +作成日: 2026-04-02 + +--- + +## 現状整理 + +### Webデータ(2026/02/23-03/22, 28日間) + +| 指標 | 値 | +|------|-----| +| MAU | 838人 | +| 総PV(推計) | 約4,200 | +| AdSense収益(3/18-22, 5日間) | 76円 | +| 月換算収益 | 約456円 | +| 推定RPM | 約108円 | +| 主要流入 | Google organic 59%(488人) | + +### 計算ツール系のPV内訳 + +| ページ | PV | 直帰率 | 特徴 | +|--------|-----|--------|------| +| 成績指標解説(/calculation-of-grades) | 228 | 27% | 登録ユーザーも参照 | +| OPS計算ツール(/tools/ops) | 203+152=355 | 55% | 最大集客ページ | +| K/BB計算ツール | 37+24=61 | 33% | エンゲージ高め | +| 出塁率計算ツール | 47+34=81 | 39% | 伸び代あり | +| 打率計算ツール | 70+39=109 | 2% | エンゲージ極高 | + +### トラフィック構造の解釈 + +計算ツール系は「ツールを使って離脱するSEO流入」と「ツールを使ったうえで登録する新規層」の2層が混在する。 +打率計算ツールの直帰率2%(109PV)は計算ツールからアプリ登録へのコンバージョンが起きている可能性が高く、価値が高い。 + +--- + +## 1. iOSアプリ収益化戦略 + +### 1-1. 基本方針 + +ターゲットが中高生(13-18歳)であることを前提に、以下の制約を前提とした設計を行う。 + +- 高額なサブスクリプションは購買意欲・保護者の承認ハードルから現実的でない +- 課金への心理的抵抗が大きい層であるため、無料体験を十分に提供してから誘導する +- 「広告を見たくない人が払う少額課金」がもっとも摩擦が小さい + +### 1-2. AdMob導入計画(短期: 1-2ヶ月以内) + +#### 技術スタック + +Expo SDK 55 + EASビルド環境では `react-native-google-mobile-ads` が標準的な選択肢。 +現在のpackage.jsonにAdMob関連ライブラリは存在しないため、新規導入が必要。 + +``` +yarn add react-native-google-mobile-ads +``` + +EASビルドが必要(Expo Go では動作不可)。現在 `expo-dev-client` が入っているため、 +EASビルドの基盤はある程度整っている。 + +iOS向けはATT(App Tracking Transparency)対応が必須。 +`expo-tracking-transparency` の導入と、app.json への `NSUserTrackingUsageDescription` 設定が必要。 + +#### 推奨フォーマットと配置場所 + +| フォーマット | 配置場所 | 期待eCPM | 備考 | +|------------|---------|---------|------| +| バナー広告(320x50) | ダッシュボード下部、試合結果一覧下部 | 50-100円 | 常時表示。CLSに注意 | +| インタースティシャル | 試合記録完了後の画面遷移時 | 200-400円 | 表示頻度を低く抑える(1日1-2回上限) | +| リワード広告(任意視聴) | 「過去の成績をもっと見る」「詳細分析を見る」 | 150-300円 | ユーザーが意図的に見る。単価最高 | + +#### インタースティシャル広告の表示制御(重要) + +中高生UXを保護するため、以下のルールを設ける。 + +- 同一セッション内で最大2回まで +- 試合記録保存直後(達成感がある瞬間)のみに限定 +- スキップボタンは5秒後に確実に表示されるよう設定 + +#### 重要な注意点: EASビルドとKotlinバージョン + +2025年時点でEASビルド時にKotlinバージョン不一致によるビルド失敗が多数報告されている。 +`react-native-google-mobile-ads` 導入後は必ずローカルビルドで動作確認してからApp Store提出を行う。 + +### 1-3. フリーミアムモデルの設計 + +#### 推奨モデル: 広告除去型の一括課金 + +月額サブスクリプションは中高生に不向き(保護者のApple ID承認、毎月の心理的コスト)。 +競合「ベボレコ」が「広告削除 ¥300」で成功しているモデルを参考にする。 + +| プラン | 価格 | 内容 | +|--------|------|------| +| 無料プラン | 0円 | 全機能使用可能 + バナー広告・インタースティシャル(制限あり) | +| プレミアムプラン | 360円(税込)の一括課金 | 広告完全非表示 | + +**360円の根拠:** +- 競合ベボレコが ¥300 で広告削除を提供している +- App Storeの最低課金単位(¥160)より上のファースト価格帯 +- 中高生が「部活のジュース1本分」と感じられる水準 +- 月額課金でなく一括のため、保護者の承認も通りやすい + +#### 段階的なアプリ内課金拡張(6ヶ月後以降の検討) + +| 機能 | 想定価格 | 提供価値 | +|------|---------|---------| +| シーズン別の詳細統計グラフ | 240円(買い切り) | 複数シーズンの推移グラフ | +| 成績PDFエクスポート | 120円(1回) | 保護者への報告、推薦入試用 | + +ただし現状DL数が不明な段階でアプリ内課金の実装コストは高い。 +まずはAdMobのみで収益データを取得してから課金機能の判断をするのが合理的。 + +### 1-4. AdMobの期待収益試算 + +| アプリDL数 | DAU(DLの5%を想定) | 月間広告収益 | +|----------|-------------------|-----------| +| 500 | 25人 | 約750円 | +| 1,000 | 50人 | 約1,500円 | +| 3,000 | 150人 | 約4,500円 | +| 10,000 | 500人 | 約15,000円 | + +計算根拠: +- DAU 1人あたり月間インプレッション: 30(バナー)+ 4(インタースティシャル) +- バナーeCPM: 70円、インタースティシャルeCPM: 300円 +- 月間収益/DAU ≒ 30円 + +スポーツ特化アプリのAdMob単価は一般アプリより若干低い傾向があるため保守的に試算。 +ATT同意率次第でeCPMは±30%変動する。 + +--- + +## 2. Web広告収益の最適化 + +### 2-1. 現状RPMの詳細分析 + +推定RPM 108円は日本スポーツ系サイトの下限水準。低い原因は構造的なものとすぐに改善できるものに分かれる。 + +| 原因 | 影響度 | 改善可否 | +|------|--------|---------| +| ターゲット層(中高生)の購買力が低い | 大 | 改善困難(コンテンツ戦略で部分的に対応) | +| アンカー広告の未使用 | 大 | すぐに改善可能(10分) | +| 計算ツールの直帰率55%(OPS) | 中 | 広告配置最適化で対応 | +| 高単価コンテンツの不足 | 中 | 中期的なコンテンツ追加で対応 | +| 試合詳細のgameResultDetailInFeedがスロットID未設定 | 小 | すぐに改善可能(5分) | + +### 2-2. 計算ツールページの最適な広告配置 + +現在の `CalculatorPageContent.tsx` の構成と問題点: + +``` +[パンくずリスト] +[H1 タイトル] +[計算フォーム + 結果] ← ユーザーの最大注目点 +[CTAバナー] ← CTAが広告より先に来るのは正しい +[AdBanner(toolsDisplay)] ← CTA下: 比較的良い位置 +[StatExplanation(解説)] +[AdBanner(toolsDetailMiddle)] ← 解説下: 可視性中程度 +[FAQ] +[CTAバナー 2個目] +[RelatedTools] +[AdBanner(toolsDetailHorizontal)] ← 最下部: 可視性低い +``` + +問題: OPS計算ツール(直帰率55%)では多くのユーザーが計算後にすぐ離脱するため、 +中段以降の広告(toolsDetailMiddle、toolsDetailHorizontal)の可視性が極めて低い。 + +#### 改善後の推奨配置(OPS・打率など主力ツール) + +``` +[パンくずリスト] +[H1 タイトル] +[リード文] +[計算フォーム] +[計算ボタン] +[計算結果 表示エリア] + ↓ 結果が出た直後の視線の動き先 +[AdBanner(rectangle 300x250)] ← 新規追加。結果確認直後のビューポート内 +[次のアクション提案 or 解説へのリンク] +[CTAバナー] +[AdBanner(toolsDisplay)] ← 既存維持 +[StatExplanation] +[AdBanner(toolsDetailMiddle)] ← 既存維持 +[FAQ] +[CTAバナー 2個目] +[RelatedTools] +[AdBanner(toolsDetailHorizontal)] ← 既存維持 +``` + +期待効果: 計算結果直後のビューポート内に広告が入ることでViewability向上。 +計算を実行したユーザーは結果を確認するため必ず視線が止まる。 +RPM +30-60円を見込む。 + +#### CLS対策(必須) + +現在の `AdBanner` の `div.ad-container` にはminHeightが設定されていない。 +広告ロード前後でのレイアウトシフトはCore Web Vitalsのスコアを下げ、AdSenseの評価にも影響する。 +新しい配置位置には必ず `minHeight: 250px` 等を設定する。 + +### 2-3. 成績指標解説ページ(/calculation-of-grades)の最適化 + +228PV、直帰率27%(エンゲージ率73%)は計算ツール系の中でもっとも滞在率が高い優良ページ。 + +推奨: タイトル・リード文の直後(ファーストビュー下部)にAdBannerを1つ追加。 +2024年以降のAdSenseは「実際に画面に表示された広告」の単価が高いため、 +ファーストビュー内への配置はRPM改善に直結する。 + +--- + +## 3. DL数・DAU・収益の相関シミュレーション + +### 前提条件 + +- アプリ→DAU転換率: 5%(業界一般的な日常利用アプリの水準) +- DAU→月間収益換算: 約30円/人(AdMob バナー+インタースティシャル) +- Web PVはアプリDL数の増加に伴い増加する(アプリユーザーがWebを参照するため) + +### 3段階シミュレーション + +#### 現在(Webのみ, アプリ未収益) + +| 収益源 | 月間収益 | +|--------|---------| +| Web AdSense(RPM 108円 x PV 4,200) | 約454円 | +| iOSアプリ AdMob | 0円 | +| 合計 | 約454円 | + +#### フェーズ1(3ヶ月後: アプリAdMob導入 + Web RPM改善) + +想定: アプリDL 500、RPM改善で180円達成 + +| 収益源 | 月間収益 | +|--------|---------| +| Web AdSense(RPM 180円 x PV 4,200) | 約756円 | +| iOSアプリ AdMob(DAU 25人) | 約750円 | +| 合計 | 約1,500円 | + +#### フェーズ2(6ヶ月後: SEO効果+アプリ成長) + +想定: アプリDL 1,500、Web PV 10,000、RPM 200円 + +| 収益源 | 月間収益 | +|--------|---------| +| Web AdSense(RPM 200円 x PV 10,000) | 約2,000円 | +| iOSアプリ AdMob(DAU 75人) | 約2,250円 | +| Amazonアソシエイト(用品記事2本) | 約2,000円 | +| 合計 | 約6,250円 | + +#### フェーズ3(12ヶ月後: スケール) + +想定: アプリDL 5,000、Web PV 30,000、RPM 250円 + +| 収益源 | 月間収益 | +|--------|---------| +| Web AdSense(RPM 250円 x PV 30,000) | 約7,500円 | +| iOSアプリ AdMob(DAU 250人) | 約7,500円 | +| アプリ内課金(広告除去 360円) | 約1,800円(月50件想定) | +| Amazonアソシエイト | 約5,000円 | +| 合計 | 約21,800円 | + +### 月間収益 1万円のハードル分析 + +Web単独の場合: PV 40,000 + RPM 250円 が最低ライン(現状の10倍のPV) +Web + アプリ併用: PV 20,000 + DAU 150人 + RPM 200円 で到達可能 + +アプリ収益は現在ゼロから始まるため、AdMob導入は収益を単純に上乗せする手段として有効。 +特にWebのSEO成長に時間がかかる間の収益ブリッジとして機能する。 + +--- + +## 4. 中高生ターゲットにおける収益化の注意点 + +### 4-1. App Store のペアレンタルコントロール対応 + +中高生の多くはファミリー共有下にある可能性があり、アプリ内課金には保護者承認が必要になるケースがある。 +一括課金 ¥360 は少額であっても承認フローが存在することを前提に、 +「買い切り」であることと「買わなくても全機能が使える」ことを明示したUI設計が重要。 + +### 4-2. AdSenseポリシーの年齢制限 + +Google AdSenseは13歳未満向けサービスへの広告配信を禁止している。 +BUZZ BASEのターゲットは中高生(概ね13歳以上)であり問題ないが、 +利用規約に13歳未満の利用禁止を明記しておくことが必要。現状の利用規約を確認・補強すること。 + +### 4-3. AdMobの子ども向けコンテンツ設定 + +AdMobのSDK設定では `tagForChildDirectedTreatment` と `tagForUnderAgeOfConsent` のフラグが存在する。 +BUZZ BASEは13歳以上を対象としているため、これらをtrueにする必要はないが、 +万一低年齢ユーザーが多いデータが出た場合は方針を再検討する。 + +### 4-4. 中高生の広告体験設計における鉄則 + +- インタースティシャルは「達成感のある瞬間の後」にのみ配置する(試合記録保存後など) +- バナー広告のサイズが小さすぎるとクリック率が低下し、大きすぎるとUXを損なう。320x50のアダプティブバナーが最適 +- リワード広告は必ず「見ることで何が得られるか」を明示してから再生する +- 広告のために機能を人質にする(広告を見ないと成績が記録できない等)は絶対にNG + +### 4-5. スマホ新法(2025年12月完全施行) + +スマートフォン競争促進法により、App Store外決済の選択肢が広がる方向にある。 +ただし現時点では安定性・信頼性の観点からApp Store標準課金を使うことを推奨する。 + +--- + +## 5. 収益化ロードマップ(フェーズ別) + +### フェーズ0: 即座に実行(所要時間: 1-2時間、今週中) + +1. AdSense管理画面でアンカー広告をON(10分) + - AdSense > 広告 > サイトごと > 自動広告 > アンカー広告をON + - インタースティシャルはOFFのまま + - 期待効果: RPM +20-40円 + +2. `adConfig.ts` の `gameResultDetailInFeed` にスロットIDを設定(5分) + - 対象: `/Users/shimizuippei/projects/dev/buzzbase/front/app/components/ad/adConfig.ts` + - AdSenseで新しいインフィード広告スロットを作成してIDを設定 + +3. AdSenseレポートでページ別RPMを確認(15分) + - どのページからの広告収益が高いか把握 + - 計算ツールページと内部ページのRPM差を確認 + +### フェーズ1: 短期(2-4週間) + +4. 計算ツールの計算結果直後に広告を追加 + - `CalculatorPageContent.tsx` で計算結果コンポーネントの直後にAdBanner(rectangle)を挿入 + - adConfig.tsに新スロット `toolsResultInline` を追加 + - 広告コンテナに `minHeight: 250px` を設定してCLS対策 + - 期待効果: RPM +30-60円 + +5. 成績指標解説ページ(/calculation-of-grades)にファーストビュー内広告を追加 + - 期待効果: RPM +15-30円 + +6. iOSアプリへのAdMob導入開始 + - `react-native-google-mobile-ads` のインストール・設定 + - ATT(App Tracking Transparency)対応 + - バナー広告をダッシュボード下部に追加 + - EASビルドでテスト後、App Store提出 + +### フェーズ2: 中期(1-2ヶ月) + +7. アプリにインタースティシャル広告を追加 + - 試合記録保存後の1箇所のみ + - 1セッションあたり最大2回の表示制限を実装 + +8. 野球用品のAmazonアソシエイト記事を2本作成 + - 「中学生 グローブ おすすめ ポジション別」(高校生向けも別記事で) + - 計算ツールと同じSEO手法を適用 + - 期待効果: 月2,000-9,000円/記事 + +9. アプリのリワード広告設計 + - 「過去シーズンの詳細グラフを見る」機能にリワード広告を紐付け + - 無料ユーザーが機能の価値を体験しつつ、広告収益も得られる設計 + +### フェーズ3: 長期(3-6ヶ月) + +10. アプリの広告除去課金(¥360)を実装 + - AdMobのデータでDAUが安定した段階で実装判断 + - RevenueCatを使ったサブスクリプション管理(将来の拡張性のため) + +11. 高単価コンテンツの追加 + - 「野球スクール・個人指導の選び方」記事(教育系広告主の高CPC) + - 「大学野球への進路・推薦入試」記事(進学系広告主の最高CPC) + +12. スポンサーシップの打診 + - 野球用品メーカー(ミズノ・ゼット等)のブランドコンテンツ + - 地域の野球スクール・クリニックへの広告掲載 + - 規模は小さくても固定単価でRPMの概念を超える収益が可能 + +--- + +## 6. 優先順位のサマリー + +収益インパクトと実装コストを総合した優先順位: + +| 優先度 | 施策 | 実装コスト | 月間収益へのインパクト | +|--------|------|-----------|----------------------| +| S | アンカー広告ON | 10分 | +180円〜 | +| S | 計算結果直後の広告追加 | 2-4時間 | +126円〜 | +| A | iOSアプリAdMob バナー導入 | 1-2日 | +750円〜(DL 500時) | +| A | gameResultDetailInFeed スロットID設定 | 5分 | 小規模だが即効性あり | +| B | Amazonアソシエイト記事1本 | 3-5時間/本 | +2,000-9,000円 | +| B | アプリ インタースティシャル追加 | 2-4時間 | +500円〜(DL 500時) | +| C | アプリ内課金(広告除去) | 1-2日 | +1,800円〜(DL 5,000時) | +| C | 高単価コンテンツ記事 | 3-5時間/本 | RPM全体を底上げ | + +### 核心的な結論 + +Web AdSenseのみではRPMをどれだけ最適化しても、PV 4,200という現状では月間収益の上限は約1,200円。 + +iOSアプリへのAdMob導入は「既存のPVに依存せず収益を追加できる」点で最優先度が高い。 +アプリDL数が伸びるほどWebとアプリの収益が相乗効果を生む構造になる。 + +Amazonアソシエイトは記事1本あたりの収益ポテンシャルがAdSenseの数十倍になる可能性があり、 +工数対効果で最も優れた中期施策。計算ツールで確立したSEOパターンを記事に転用できる。 + +月間収益1万円の現実的なルート: +PV 15,000(SEO継続)+ アプリDAU 100人(AdMob)+ アフィリエイト記事3本 = 月間約10,500円 diff --git a/docs/strategy/product/ios-app-download-growth.md b/docs/strategy/product/ios-app-download-growth.md new file mode 100644 index 0000000..87572be --- /dev/null +++ b/docs/strategy/product/ios-app-download-growth.md @@ -0,0 +1,352 @@ +# iOSアプリDL数増加 - 機能企画・導線設計 + +作成日: 2026-04-02 + +--- + +## 現状分析 + +### Web計算ツールのトラフィック(実測値) + +| ページ | 月間表示 | 月間クリック | 直帰率 | +|--------|---------|------------|--------| +| OPS計算ツール | 10,877 | 166 | 55% | +| 成績指標解説 | 6,979 | 103 | - | +| 新規会員登録 | 314PV | 170ユーザー | - | + +### 現状の課題 + +- 計算ツールへの月間10,000超の表示に対してアプリDLへの転換が計測できていない +- CtaBannerは計算ツールページ上下に既に配置されているが、計算完了前(入力フォームの上流)にある +- 招待機能が「フォロー中ユーザー限定」のため、アプリ外への口コミ拡散が起きない +- Smart App Bannerが未実装(Issue #195 登録済み、仕様未定) + +--- + +## 施策1: 計算ツール → アプリDL コンバージョン改善 + +### 1-1. 計算結果連動型CTAバナー(Issue #174 拡張) + +**現状**: CtaBannerは固定文言でページ上下に配置 +**課題**: 計算完了前に表示されるCTAは動機が弱い。計算結果が出た瞬間が最もモチベーションが高い + +**提案: 計算結果直後に「この成績をアプリで記録する」CTAをインライン表示** + +``` +[計算フォーム] + ↓ 計算実行 +[計算結果: OPS 0.850] + ↓ 結果の直下に挿入 +[-------- CTA --------] +| OPS 0.850 を記録する | +| アプリなら毎試合記録するだけで| +| OPS・打率・出塁率を自動計算 | +| [App Storeでダウンロード] | +[---------------------] +``` + +**フック文言案** +- OPS計算: 「OPS 0.XXXを記録した。アプリならシーズン全試合の推移をグラフで確認できます」 +- 打率計算: 「この打率をチームランキングで比較してみよう」 +- 防御率計算: 「防御率X.XXを仲間に見せよう。チームランキングでポジション争いできます」 + +**実装方針** +- 各Calculator Componentは現在 Client Component(インタラクティブな計算フォーム) +- 計算実行後に結果値を受け取り、動的な文言を生成する props を CtaBanner に追加 +- `CtaBanner` に `resultValue?: string` と `resultMessage?: string` の props を追加 + +**工数見積もり**: S(半日〜1日) +計算ツール9本に対してCtaBannerの props を変更するだけ。各CalculatorコンポーネントにCTA props を渡す対応。 + +--- + +### 1-2. Smart App Banner 仕様定義(Issue #195 の仕様補完) + +**実装方式**: Apple標準の `<meta name="apple-itunes-app">` タグを使用 + +```html +<meta name="apple-itunes-app" content="app-id=XXXXXXX, app-argument=https://buzzbase.jp/tools/ops"> +``` + +**配置**: `front/app/layout.tsx` のグローバルmetadataに追加 +iOS Safariでのみ自動表示される。Androidには影響しない。 + +**app-argument の使い方** +- 計算ツールページからインストールした場合、アプリ起動時に該当ツールの記録画面へのDeep Link遷移を試みる +- 初期はapp-argumentなしの基本実装で十分 + +**設定箇所** +``` +front/app/layout.tsx + → metadata.other に apple-itunes-app を追加 +``` + +**工数見積もり**: XS(2〜3時間) +App IDの確認とmetadataへの1行追加のみ。 + +--- + +### 1-3. 計算ツールページのアプリ誘導強化(モバイル表示限定) + +**提案**: モバイルブラウザでの閲覧時に限り、ページ最上部に固定バナーを表示 + +``` +[BUZZ BASE アプリで成績を自動計算 → App Store] × 閉じる +``` + +**Smart App Banner との違い** +- Smart App Bannerはブラウザ標準UIでデザインが変えられない +- カスタムバナーはデザイン・文言を完全にコントロールできる +- ただし「バナー2枚」になるのでどちらか一方に絞る判断も検討 + +**工数見積もり**: S(半日) +`useUserAgent` でモバイル判定し、Session Storage で「閉じた」状態を管理するクライアントコンポーネントとして実装。 + +--- + +## 施策2: アプリDLを促進する機能企画 + +### 2-1. アプリ限定機能の明示(Web版での"のぞき見"UI) + +**課題**: Webサイトではアプリの中身が見えないためDLの動機が生まれにくい + +**提案: 計算ツールページに「アプリ機能プレビュー」セクションを追加** + +イメージとして以下のスクリーンショット的なUIを静的コンテンツとして表示する。 + +``` +━━━━━━━━━━━━━━━━━━━ +アプリでできること +━━━━━━━━━━━━━━━━━━━ +[スクリーンショット画像] [スクリーンショット画像] + 試合ごとに成績を記録 チームランキングで比較 + 打率・OPS・防御率を 友達と競い合って + 自動で計算 モチベーションUP +━━━━━━━━━━━━━━━━━━━ +``` + +**工数見積もり**: S(半日〜1日) +App Storeスクリーンショット画像を使い回す静的コンポーネント。 + +--- + +### 2-2. 成績シェア機能(アプリ → SNS拡散) + +**概要**: アプリ内で成績カードを生成し、LINEやXにシェアできる機能 + +**シェアされるコンテンツ例** +``` +【BUZZ BASE】 +今シーズンの成績 +打率: .312 +OPS: 0.892 +出塁率: .378 +長打率: .514 + +#buzzbase #野球 #高校野球 +https://buzzbase.jp +``` + +**実装方法** +- React Native の `Share API`(Expo Sharing)を使用 +- テキストシェアは即座に実装可能 +- 画像カード生成(react-native-view-shot)は工数が大きいため後回し + +**シェアのトリガー** +- 試合結果記録完了後の「シェアする」ボタン +- マイページの「成績をシェア」ボタン +- グループランキング1位獲得時の祝福モーダルにシェアボタン + +**工数見積もり**: S(テキストシェアのみ、1日) + +--- + +### 2-3. アプリ内「計算ツール」タブ(Web機能のアプリ統合) + +**背景**: SEO流入の主力である計算ツールはWebのみ。アプリユーザーには提供されていない。 + +**提案**: アプリ内に計算ツールを組み込み、計算結果をそのまま試合記録に反映できる導線を作る + +**ユーザーフロー** +``` +アプリ内計算ツール → OPS 0.850 を計算 + ↓ +「この成績を記録する」ボタンを押す + ↓ +試合記録入力画面に計算元の数値(安打数・打数等)が自動入力された状態で遷移 +``` + +**意義** +- 「Webで計算してアプリで記録」という分断されたUXを統合できる +- アプリ内での滞在時間増加 +- 新規ユーザーがアプリを開く理由になる(「計算できるアプリ」という認知) + +**工数見積もり**: M(2〜3日) +Web版の計算ロジックはTypeScriptで実装済みのため、モバイル側に移植する工数が主体。 + +--- + +## 施策3: グループ招待URL によるバイラル施策(Issue #197 活用) + +### 3-1. 招待URLの設計 + +**URL形式** +``` +https://buzzbase.jp/invite/g/{token} +``` + +**トークン仕様** +- UUID v4 または 8文字のランダム文字列 +- 有効期限: 7日間(招待URLの鮮度を保つ) +- 1グループにつき同時に有効なトークンは1件(再生成で古いトークンは無効化) + +**未登録ユーザーがURLを踏んだ時のフロー** +``` +招待URL(buzzbase.jp/invite/g/{token}) + ↓ +Web版のランディングページ(招待内容 + グループ概要 + アプリDL誘導) + ↓ App Storeへ +iOSアプリDL・インストール + ↓ +アプリ初回起動 → サインアップ画面 + ↓(サインアップ完了) +招待グループへの自動参加(保留中ステータスで保存、サインアップ時に紐付け) +``` + +**招待LP(Web版)の構成要素** +- 「{招待者名}さんがBUZZ BASEグループへ招待しています」というヘッダー +- グループ名・メンバー数の表示 +- 「アプリでグループに参加する」ボタン(App Storeへのリンク) +- BUZZ BASEのサービス紹介(3点) +- 登録済みユーザー向け「アプリを開く」ディープリンク + +**工数見積もり(バックエンド)**: M(2〜3日) +- `group_invitation_tokens` テーブル追加(group_id, token, expires_at, created_by) +- `GET /api/v1/invitations/g/:token` エンドポイント(トークン情報返却) +- `POST /api/v1/invitations/g/:token/accept` エンドポイント(グループ参加) +- `POST /api/v1/groups/:id/invitation_token` エンドポイント(トークン生成) +- サインアップ時の招待トークン紐付け処理 + +**工数見積もり(フロントエンド・Web)**: S(1日) +- `/invite/g/[token]` ページ(招待LP) + +**工数見積もり(モバイル)**: S(1日) +- 招待URL共有ボタン(`Share.share()`) +- サインアップ時のトークン保持(`SecureStore` に保存) + +--- + +### 3-2. シェア文言の最適化 + +**LINE向け(文章重視)** +``` +一緒にBUZZ BASEで成績を管理しよう! +野球の成績を記録するだけで、打率・OPS・防御率が自動で計算されます。 +グループ内のランキングで競い合いましょう。 + +グループに参加する→ https://buzzbase.jp/invite/g/XXXX +``` + +**X(Twitter)向け(短文)** +``` +BUZZ BASEで成績を一緒に管理しよう #buzzbase +https://buzzbase.jp/invite/g/XXXX +``` + +--- + +## 施策4: Smart App Banner 実装仕様(Issue #195 の具体化) + +### 実装内容 + +**対象ファイル**: `front/app/layout.tsx` + +**追加するメタタグ** +```typescript +// metadata.other に追加 +other: { + 'apple-itunes-app': `app-id=${process.env.NEXT_PUBLIC_APP_STORE_APP_ID}`, +}, +``` + +**表示条件** +- iOS Safariでのみブラウザが自動表示(他ブラウザには影響なし) +- ユーザーが「×」で閉じると30日間非表示になる(Apple仕様) + +**追加設定(任意)** +- `app-argument` にページURLを渡すことでDeep Link対応が可能 +- 初期リリースは基本実装(app-argumentなし)で十分 + +**環境変数** +- `NEXT_PUBLIC_APP_STORE_APP_ID` にApp Store ID(数字10桁)を設定 + +**工数見積もり**: XS(1〜2時間) +App IDは App Store Connect で確認可能。 + +--- + +## 施策5: App Store スクリーンショットで映える画面の提案 + +### 優先度順の画面リスト + +| 優先度 | 画面 | 訴求ポイント | 対応アプリ画面 | +|--------|------|-----------|-------------| +| **1** | グループランキング画面 | 「仲間と競い合う」体験を一目で伝える | `(groups)/[id].tsx` | +| **2** | ダッシュボード(成績サマリー) | OPS・打率など全指標が見やすく並ぶ | `(tabs)/index.tsx` | +| **3** | 試合記録入力画面(Step完了後の確認画面) | 「かんたん入力」の手軽さを表現 | `(game-record)/` | +| **4** | 成績トレンドグラフ(実装後) | 成長が見える。長期利用のイメージが湧く | 未実装 | +| **5** | プロフィール画面(成績カード) | 個人の実績が格好よく表示される | `(profile)/index.tsx` | + +### スクリーンショットのデザイン方針 + +- ダークテーマ(背景 #2E2E2E)がそのまま使えるためApp Storeで目立つ +- ランキング画面は「1位」の状態でデモデータを用意する(競争感を演出) +- 数字はリアルな中高生の成績レンジで設定(打率: .280〜.320、防御率: 2.50〜3.50) +- 「試合結果を記録する」ボタンが目立つダッシュボード画面はファーストビューに最適 + +### スクリーンショット用デモデータ + +アカウント作成直後でも映える画面を見せるため、App Storeレビュー申請時のみ使用するデモシードデータを用意する。 + +``` +デモユーザー: +- 田中 翔(ショート、県立○○高校) +- 成績: 打率 .312 / OPS 0.892 / 出塁率 .378 +- グループ: 「〇〇高校野球部 2024」にて2位(1位との差が僅差の状態) +``` + +--- + +## 施策の優先順位と工数サマリー + +| 優先度 | 施策 | 工数 | 期待効果 | 担当コンポーネント | +|--------|------|------|---------|----------------| +| **P0** | Smart App Banner 実装 | XS (2h) | 全ページからiOSユーザーへの受動的誘導 | `front/app/layout.tsx` | +| **P0** | 計算結果連動型CTA | S (1日) | 計算ツール利用者の体験に合わせた誘導 | `CtaBanner` + 各Calculator | +| **P1** | グループ招待URL | M (5〜7日) | 既存ユーザーの口コミ拡散の起点を作る | back + front + mobile | +| **P1** | 成績シェア機能(テキスト) | S (1日) | SNS経由のオーガニック流入 | mobile | +| **P2** | アプリ機能プレビューセクション | S (1日) | 計算ツール流入ユーザーへの訴求 | `CalculatorPageContent` | +| **P2** | アプリ内計算ツール | M (3日) | アプリの独自価値向上 | mobile | +| **P3** | カスタムモバイルバナー | S (半日) | Smart App Banner の補完 | front | + +--- + +## GitHub Issue 起票候補 + +以下の Issue を新規作成する。既存 Issue (#195, #196, #197) とは重複しない形で設計する。 + +### Issue A: 計算結果連動型CTAバナーの実装 +- 各計算ツールの計算完了時に、結果値を含む動的文言のCTAを表示する +- Issue #174(CTAバナー文言カスタマイズ)を拡張する形で対応 + +### Issue B: グループ招待URL機能の詳細仕様 +- Issue #197 に対するバックエンド・フロント・モバイルの具体的な実装仕様 + +### Issue C: 成績テキストシェア機能 +- アプリから成績をLINE/Xにシェアできるボタンを実装 + +### Issue D: App Storeスクリーンショット用デモシードデータの整備 +- App Store審査・スクリーンショット撮影用のデモデータ整備 + +### Issue E: 計算ツールページへのアプリ機能プレビューセクション追加 +- 静的画像でアプリの機能を紹介するセクション diff --git a/docs/strategy/product/web-to-app-conversion-202604.md b/docs/strategy/product/web-to-app-conversion-202604.md new file mode 100644 index 0000000..f7437a6 --- /dev/null +++ b/docs/strategy/product/web-to-app-conversion-202604.md @@ -0,0 +1,548 @@ +# Web→アプリ登録ファネル 導線設計・CTA企画 + +作成日: 2026-04-30 + +--- + +## 現状サマリー + +| 指標 | 値 | +|------|-----| +| Web総セッション | 1,723/月(mobile 1,358、desktop 365) | +| TOPページ App Store CTR | 11%(健全) | +| /signup ページ App Store CTR | 4%(弱い) | +| 計算ツール系合計PV | 約1,000/月 | +| 計算ツール App Store クリック | 約12件(CTR 1.2%) | +| 主要流入クエリ意図 | アプリ探索(「野球 個人成績 アプリ 無料」357impr/CTR15%) | + +### 既実装済みCTA(重複企画を避けるための確認) +- `SmartAppBanner`: 全ページ上部固定(7日localStorage dismiss、Client Component) +- `CtaBanner`: 計算ツールページ上下に2箇所(Server Component、固定文言) +- `CalculatorForm`: 計算結果表示直後にインラインCTAボタンを動的表示 +- `apple-itunes-app` meta タグ: iOS Safariネイティブバナー有効化済み + +--- + +## 施策1: SmartAppBanner と iOS Safari Smart App Bannerの重複問題解消 + +### タイトル +iOS Safariネイティブバナーと自前SmartAppBannerの重複解消 + +### 背景(なぜ必要か、データ根拠) +- iOS Safariは `<meta name="apple-itunes-app">` を検出すると独自のSmartAppBannerをページ上部に自動表示する +- 現在の実装では自前の `SmartAppBanner`(黒帯バナー)と、iOSネイティブバナーが同時に表示される場合がある +- ネイティブバナーはOSレベルで信頼感が高くCTRも高いが、自前バナーと重なると視覚的に邪魔になりUXを損なう +- mobile PV比率が78%(1,358/1,723)のため、iOS Safariユーザーへの影響が大きい + +### 実装内容 +- `SmartAppBanner.tsx` の `useEffect` 内に iOS Safari のStandalone判定とUserAgent判定を追加する +- iOSネイティブバナーが表示されている状態(= `navigator.standalone !== true` かつ iOS Safari)では自前バナーを非表示にする +- 代替案: iOS Safari向けには表示せず、Androidブラウザ・デスクトップ Chrome向けにのみ自前バナーを出す + +```typescript +// SmartAppBanner.tsx useEffect内に追加するロジック例 +const isIOS = /iPhone|iPad|iPod/.test(navigator.userAgent); +const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); +if (isIOS && isSafari) { + // iOSネイティブバナーに任せる + setVisible(false); + return; +} +``` + +### 受け入れ条件 +- iOS SafariでURLを開いたとき、自前バナーと Apple ネイティブバナーが同時に表示されない +- Android Chrome・デスクトップブラウザでは自前バナーが引き続き表示される +- dismissしたユーザーには7日間非表示のロジックが維持される + +### 工数見積もり +0.5日 + +### 期待効果 +- iOS Safariユーザーのバナー領域UX改善 +- ネイティブバナーのCTRが自前バナーより高い傾向があるため、アプリDL +3〜5件/月の改善 + +### 優先度 +**A** + +--- + +## 施策2: 計算ツール結果CTAの個別化文言(成績レベル連動) + +### タイトル +計算ツール結果CTA文言の個別化(計算値のレベル判定に応じた訴求) + +### 背景(なぜ必要か、データ根拠) +- 計算ツール合計CTR 1.2%は低い。現行の `CalculatorForm` 内インラインCTAは「アプリで成績を記録する(無料)」という固定文言 +- ユーザーが計算結果を見た直後が最もモチベーションが高い瞬間。このタイミングで計算値を反映した個別化文言を出すことで共感を生みやすい +- 例: OPS .850 → 「OPS .850!高校野球トップレベルの成績。アプリで毎試合の推移を記録しよう」 +- 例: 打率.220 → 「打率.220、まだ伸びしろあり。アプリで試合ごとの成績を記録して改善しよう」 +- guide(レベル判定テーブル)が `calculator-definitions.ts` に既に定義されているため、実装コストが低い + +### 実装内容 + +**`CalculatorForm.tsx` の変更**: +- `results` に加えて `levelLabel`(guide配列からマッチしたdescription)を導出する +- インラインCTA部分に `levelLabel` と計算値(`results[0].value`)を組み合わせた文言を生成する +- 文言生成ロジックは `CalculatorForm` に直接書かず、`getCtaMessage(slug, value, levelLabel)` のようなユーティリティ関数に切り出す +- 既存の `CtaBanner`(ページ上下の静的バナー)とは別レイヤーで管理する + +**文言テンプレート案(各ツール)**: +``` +OPS計算: "OPS {value}({level})。アプリで毎試合の推移グラフを確認しよう" +打率計算: "打率{value}({level})。試合ごとに入力してシーズン通算を自動集計" +防御率計算: "防御率{value}({level})。アプリで自責点・投球回を記録して推移を管理" +K/BB計算: "K/BB{value}({level})。制球力の推移をアプリで見える化しよう" +``` + +**`data-cta` 属性の追加**(施策6と連携): +```tsx +<a + href={APP_STORE_URL} + data-cta={`calculator_result_${slug}`} + ... +> +``` + +### 受け入れ条件 +- 計算実行後、結果値とレベル説明を組み合わせたCTA文言が表示される +- guide定義のないツール・マッチしないレベルの場合はフォールバック文言が表示される +- `data-cta` 属性がCTAボタンに付与されている + +### 工数見積もり +1日(文言定義含む) + +### 期待効果 +- 計算ツールCTR 1.2% → 3%への改善(+20件/月) + +### 優先度 +**S**(最高インパクト・低工数) + +--- + +## 施策3: /tools/k-bb のCTA計測欠落の原因究明 + +### タイトル +/tools/k-bb のApp Storeクリック「0件」の原因調査 + +### 背景(なぜ必要か、データ根拠) +- 「/tools/k-bb のApp Storeクリックが0件(CTR 0%)」と報告されているが、コードを確認した結果、以下が判明している + - `KBBCalculator.tsx` は他ツールと同様に `CalculatorForm` を使用しており、結果表示後のインラインCTA表示ロジックは存在する + - `calculator-definitions.ts` の k-bb エントリには `cta` フィールドが定義されており、`CalculatorPageContent` 経由の `CtaBanner` も表示されている + - 構造上、CTAが「欠落している」ことはなく、**トラッキングタグ(`data-cta`)が全CTAに未付与**のため、計測できていない可能性が高い +- ただし、以下の可能性も排除できない + - ページPV自体が少なく、計算実行まで到達しているユーザーが極めて少ない(直帰率50%) + - `calculate` 関数に `walks === 0` の場合 `null` を返すロジックがあり、「与四球0」と入力したユーザーでエラーが出て離脱している + +### 実装内容(調査タスク) +1. GA4 または Search Console でk-bbページの実際の流入クエリとPVを確認 +2. 全CTAに `data-cta` 属性を付与してクリック計測を開始(施策6と同時実施) +3. 「与四球=0」入力時のエラー文言を「与四球が0の場合K/BBは計算できません(∞扱い)」と明示的に変更する + +### 受け入れ条件 +- k-bbページの App Store クリックが GA4 または類似ツールで計測できるようになっている +- 与四球0入力時にユーザーが理解できるエラーメッセージが表示される + +### 工数見積もり +0.5日(調査)+ 0.5日(エラー文言修正)= 1日 + +### 期待効果 +- 問題の正確な把握により適切な改善施策の立案が可能に +- エラー文言改善で計算完了率が向上 → CTA表示機会増加 + +### 優先度 +**A** + +--- + +## 施策4: /signup ページのアプリ誘導強化 + +### タイトル +/signup ページのアプリ優先誘導UI追加(Web登録完了後DL促進 + 登録前アプリ提案) + +### 背景(なぜ必要か、データ根拠) +- /signup ページのApp Store CTR が 4%(月150PV × 4% = 月6件のクリック) +- 現在の実装: `CtaBanner` がSignUpフォームの下に1つあるだけ(body文言が「アプリならもっと便利に成績を記録・管理できます。」と弱い) +- 「野球 個人成績 アプリ 無料」で検索して着地したユーザーはアプリへの関心が高いが、Web登録フォームを見て迷っている可能性がある +- 戦略的論点: Web登録完了後にアプリDLを促す「Web登録→アプリDL誘導」と、「最初からアプリへ誘導」のどちらが効果的か + +**戦略判断: 両方実施する(段階的アプローチ)** +- フォーム上部に「iPhoneをお使いの方はアプリから登録がおすすめ」という軽量なバナーを追加(アプリ優先の選択肢を提示) +- Web登録完了後の成功画面(またはトースト)でアプリDLへの導線を追加 + +### 実装内容 + +**A. 登録フォーム上部にアプリ優先バナー(Server Component)**: +- `/signup/page.tsx` の SignUpフォームの上に小さいバナーを追加 +- 文言案: 「iPhoneユーザーはアプリからの登録がスムーズです(30秒・完全無料)」 +- App Store リンク付きのシンプルなボックス(CtaBannerより軽量) + +**B. 登録完了後アプリDL誘導(既存SignUpコンポーネントの拡張)**: +- `SignUp` コンポーネントの登録成功時のコールバック後にアプリDLモーダルまたはページ遷移を追加 +- または `/signup/complete` ページを新設して登録完了後にリダイレクト + +``` +/signup/complete/page.tsx: + - 「登録完了!」メッセージ + - App Store ダウンロードボタン(大きく) + - 「Webで続ける」リンク(小さく) +``` + +### 受け入れ条件 +- /signup ページにアプリ優先誘導バナーが表示されている +- Web登録完了後にアプリDLを促す画面またはモーダルが表示される +- 「Webで続ける」の導線が残っており、Web登録フローが完全に壊れない + +### 工数見積もり +- A(フォーム上部バナー): 0.5日 +- B(登録完了後誘導): 1.5日 +- 合計: 2日 + +### 期待効果 +- /signup CTR 4% → 15%への改善(月150PV → +17件/月) + +### 優先度 +**A**(登録意向の高いユーザーへのラストマイル施策) + +--- + +## 施策5: 未実装計算ツール・ナレッジ記事の追加(SEO流入拡大) + +### タイトル +Search Console上位クエリに対応する計算ツール・記事ページの新規作成 + +### 背景(なぜ必要か、データ根拠) +- 「野球 個人成績 アプリ 無料」(357impr / CTR 15%)など、アプリ探索クエリで上位表示できている +- 「長打率 計算」「k/bb 野球」「自責点 失点 違い」などの検索クエリは計算ツールとナレッジ記事の両方で攻略可能 +- 既存の計算ツールが11種あり、関連するコンテンツを増やすことでサイト全体のトピカルオーソリティを高められる +- 計算ツールは `calculator-definitions.ts` にエントリを追加するだけで実装できるため工数が低い + +### 実装内容 + +**A. 未実装の可能性が高い計算ツール候補**: +| ツール名 | 検索クエリ | 既存状況 | +|---------|-----------|--------| +| FIP計算ツール | 「FIP 野球 計算」 | 未実装 | +| BB/9計算ツール | 「BB/9 計算」 | 未実装 | +| RC(得点創出)計算ツール | 「RC 野球 計算」 | 未実装 | +| 盗塁成功率計算ツール | 「盗塁成功率 計算」 | 未実装 | + +**B. 記事コンテンツ候補(`/column/` 配下)**: +| 記事タイトル | 検索クエリ | 期待PV | +|------------|-----------|------| +| 「自責点と失点の違いを徹底解説」 | 「自責点 失点 違い」 | 月200〜500PV | +| 「野球の個人成績管理アプリ徹底比較2026」 | 「野球成績 アプリ 比較」 | 月300〜800PV | +| 「高校野球の成績管理ノートアプリ おすすめ」 | 「野球 記録 アプリ」 | 月200〜500PV | +| 「OPS・wOBA・FIPまとめ:セイバーメトリクス入門」 | 「セイバーメトリクス 野球 指標」 | 月100〜300PV | + +**「野球の個人成績管理アプリ徹底比較」記事の戦略的価値**: +- 比較記事はトランザクショナル検索(アプリをDLする直前の検索)に応答できる +- BUZZ BASEを筆頭に比較しつつ、機能面の優位性を訴求できる +- アプリへの直接誘導CTAを記事内に組み込める + +### 受け入れ条件 +- 計算ツール: 新規ツールページが `/tools/{slug}` で公開され、sitemap.xmlに追加されている +- 記事: `/column/` 配下に新規ページが公開されており、構造化データ(Article)が付与されている +- 各ページにCtaBannerが配置されている + +### 工数見積もり +- 計算ツール1本: 0.5日(定義追加 + ページ作成) +- 記事1本: 1〜2日(本文執筆 + 実装) +- 優先4本分: 5〜8日 + +### 期待効果 +- SEO流入増加で月+200〜500PV(3ヶ月後)→ アプリDL +5〜15件/月 + +### 優先度 +**B**(中期施策、SEOは時間がかかる) + +--- + +## 施策6: ランキングWebプレビューページ(未ログインユーザー向け) + +### タイトル +グループランキングのWebプレビュー機能(未登録ユーザーへのデモ表示) + +### 背景(なぜ必要か、データ根拠) +- `/groups/[slug]/page.tsx` は `useRequireAuth()` で認証必須となっており、未ログインユーザーはランキングを一切見られない +- `/mypage/[slug]/page.tsx` も未ログイン時は「成績・試合情報を閲覧するにはログインが必要です」とオーバーレイ表示 +- 「自分も載りたい」「チームランキングを見てみたい」というFOMO(機会損失恐怖)を未登録者に感じさせる導線がない +- Webからアプリ登録へのファネルで最も欠けているのは「サービスの価値を体験させるステップ」 + +### 実装内容 + +**A. ランキングデモページ `/ranking/demo` の新規作成(Server Component)**: +- モックデータを使ったランキングテーブルを表示(実データ不使用) +- 打撃・投手ランキングをタブ切り替えで表示 +- 表のいくつかのセルをぼかし処理(CSS blur)して「続きを見るにはアプリ登録が必要」と訴求 +- ページ下部に大きなApp Store CTAバナー + +**B. `/mypage/[slug]` の未ログイン時表示改善**: +- 現在のオーバーレイ上にApp Storeへの誘導を追加 +- 「このユーザーの成績を見るにはアプリ登録が必要です」→「アプリで無料登録 →」ボタン + +**モックデータ設計**: +```typescript +// app/(app)/ranking/demo/_data/mock-ranking.ts +const mockBattingRanking = [ + { rank: 1, name: "田中 一郎", team: "〇〇高校", battingAverage: ".380", hits: 19 }, + { rank: 2, name: "鈴木 二郎", team: "△△中学", battingAverage: ".340", hits: 17 }, + // ... 5〜8件 +]; +``` + +### 受け入れ条件 +- `/ranking/demo` ページが未ログインユーザーでもアクセスできる +- ランキングの一部(上位3件程度)は表示され、残りはブラー/マスク処理 +- App Store CTAが目立つ位置に配置されている +- `/mypage/[slug]` の未ログイン時にApp Store誘導ボタンが表示される + +### 工数見積もり +- Aデモページ: 2日 +- B mypage改善: 0.5日 +- 合計: 2.5日 + +### 期待効果 +- サービス価値の可視化によりアプリDL +5〜10件/月(3ヶ月後) + +### 優先度 +**B**(インパクトは大きいが実装量も多い) + +--- + +## 施策7: 全CTAへの `data-cta` 属性付与規約の策定と実施 + +### タイトル +CTA計測規約(`data-cta` 属性統一)の策定と全CTAへの適用 + +### 背景(なぜ必要か、データ根拠) +- 現状、どのCTAが何件クリックされているか計測できていない +- 計算ツール全体でApp Storeクリック12件/月と分かっているが、どのツールのどのCTA(インラインか静的バナーか)からのクリックか不明 +- /tools/k-bb の「クリック0件」も、計測できていないだけで実際はクリックされている可能性がある +- 施策の効果測定ができなければPDCAが回せない + +### 実装内容 + +**命名規約**: +``` +data-cta="{場所}_{コンテキスト}" + +例: + data-cta="smart_banner_top" // SmartAppBannerのApp Storeリンク + data-cta="cta_banner_calculator_batting-average" // 打率ページのCtaBanner(上) + data-cta="calculator_result_ops" // OPS計算結果直後のインラインCTA + data-cta="signup_page_top_banner" // signupページ上部バナー + data-cta="signup_complete_main" // 登録完了ページメインCTA + data-cta="ranking_demo_bottom" // ランキングデモページCTA + data-cta="mypage_unauthenticated" // mypageの未ログインCTA +``` + +**対象ファイルと変更箇所**: +| ファイル | 変更箇所 | +|---------|---------| +| `SmartAppBanner.tsx` | App Storeリンクの `<a>` タグ | +| `CtaBanner.tsx` | App Storeリンクの `<a>` タグ(`ctaId` propを追加) | +| `CalculatorForm.tsx` | インラインCTAの `<a>` タグ(`slug` propを追加) | +| `/signup/page.tsx` | 新設バナーのリンク | +| 将来実装のページ | 都度適用 | + +**`CtaBanner.tsx` の型定義変更**: +```typescript +type Props = { + heading?: string; + body: string; + className?: string; + ctaId: string; // 追加(必須化) +}; +``` + +### 受け入れ条件 +- 全てのApp StoreリンクにURL以外で識別可能な `data-cta` 属性が付与されている +- GA4 または類似ツールのカスタムイベントでclickイベントを計測できる +- CTAId命名規約がこのドキュメントに記載されており、新規実装時に参照できる + +### 工数見積もり +0.5日(既存ファイルの属性追加のみ) + +### 期待効果 +- 計測基盤の整備により、以降の施策のA/Bテストと効果測定が可能になる +- 間接的にアプリDL +X件(施策の最適化を通じて) + +### 優先度 +**S**(他の施策の前提条件。最初に実施すべき) + +--- + +## 実装優先度サマリー + +| 優先度 | 施策 | 工数 | 期待DL増(/月) | +|--------|------|------|---------------| +| S | 施策7: data-cta属性規約と適用 | 0.5日 | 計測基盤(間接効果) | +| S | 施策2: 計算結果CTA文言個別化 | 1日 | +20件 | +| A | 施策1: SmartAppBanner重複解消 | 0.5日 | +3〜5件 | +| A | 施策3: k-bb CTA計測調査 | 1日 | 計測精度向上 | +| A | 施策4: /signup アプリ誘導強化 | 2日 | +17件 | +| B | 施策5: 未実装ツール・記事追加 | 5〜8日 | +5〜15件(3ヶ月後) | +| B | 施策6: ランキングWebプレビュー | 2.5日 | +5〜10件(3ヶ月後) | + +### 推奨実施順序 +1. 施策7(data-cta規約)→ 計測基盤を先に整える +2. 施策2(計算結果CTA個別化)→ 既存トラフィックへの即効性 +3. 施策4(signup誘導強化)→ 登録意向ユーザーへのラストマイル +4. 施策1(SmartAppBanner重複解消)→ UX改善 +5. 施策3(k-bb調査)→ 施策7と同時実施可 +6. 施策5 / 施策6 → 中期でPDCAしながら + +--- + +## GitHub Issue 起票案 + +以下5本のIssueを起票する。 + +--- + +### Issue 1: [Add] 全CTAにdata-cta属性を付与してApp Storeクリックを計測できるようにする + +**本文**: + +#### 背景 +計算ツール全体のApp Storeクリックが月12件と分かっているが、どのページのどのCTAからのクリックかが計測できていない。施策の効果測定・改善のためにCTAクリックの計測基盤を整備する。 + +#### タスク + +**命名規約** +`data-cta="{場所}_{コンテキスト}"` の形式で統一する。 + +例: +- `data-cta="smart_banner_top"` — SmartAppBanner +- `data-cta="cta_banner_{slug}"` — CtaBannerのツール別 +- `data-cta="calculator_result_{slug}"` — 計算結果直後のインラインCTA + +**変更対象ファイル** +- `front/app/(app)/_components/SmartAppBanner.tsx`: App Storeリンク `<a>` に `data-cta="smart_banner_top"` 追加 +- `front/app/(app)/_components/CtaBanner.tsx`: `ctaId: string` propを追加(必須)し、リンクに `data-cta={ctaId}` を付与 +- `front/app/(app)/tools/_components/CalculatorForm.tsx`: `slug: string` propを追加し、インラインCTAに `data-cta={`calculator_result_${slug}`}` を付与 +- `CtaBanner` の呼び出し元(`CalculatorPageContent.tsx` 等)に `ctaId` を渡す修正 + +#### 受け入れ条件 +- [ ] 全てのApp StoreリンクにURL以外で識別可能な `data-cta` 属性が付与されている +- [ ] `CtaBanner` に `ctaId` propが追加され、呼び出し元で指定されている +- [ ] `CalculatorForm` に `slug` propが追加されている + +#### 工数見積もり +0.5日 + +--- + +### Issue 2: [Add] 計算ツールの結果CTA文言を計算値とレベルに応じて個別化する + +**本文**: + +#### 背景 +計算ツール全体のApp Store CTRが1.2%と低い。計算完了直後が最もユーザーのモチベーションが高い瞬間であるにもかかわらず、現行のインラインCTA(`CalculatorForm.tsx` 内)は「アプリで成績を記録する(無料)」という固定文言のみ。計算結果の値とレベル判定(guide配列)を組み合わせた個別化文言に変更することで、共感を生みCTRを高める。 + +#### 実装内容 + +1. `CalculatorForm` のpropsに `slug: string` を追加する(施策7のIssue 1と同時実施) +2. `calculator-definitions.ts` の `guide` 配列から計算値に対応する `description` を導出するユーティリティ関数 `getStatLevel(guide, value)` を `app/utils/getStatLevel.ts` に作成する +3. `CalculatorForm.tsx` の計算結果表示後のインラインCTA文言を、`{results[0].value}({levelDescription})アプリで毎試合の推移を記録しよう` 形式に変更する +4. guide定義がないツールやマッチしない値の場合は既存の固定文言にフォールバックする + +#### 受け入れ条件 +- [ ] 計算実行後、計算値とレベル説明を組み合わせたCTA文言が表示される +- [ ] guide定義のないツールでフォールバック文言が表示される +- [ ] `data-cta` 属性がCTAボタンに付与されている(Issue 1と連携) +- [ ] TypeScriptの型エラーがない + +#### 工数見積もり +1日 + +--- + +### Issue 3: [Fix] /tools/k-bb のApp Storeクリック計測ゼロの原因調査と修正 + +**本文**: + +#### 背景 +`/tools/k-bb` のApp Storeクリックが「0件」と報告されているが、コードを確認すると `KBBCalculator.tsx` は他ツールと同様に `CalculatorForm` を使用しており、CTAの実装漏れはないと思われる。原因を特定して修正する。 + +#### 調査項目 + +1. **計測問題の可能性**: 全CTAに `data-cta` 属性が未付与のため、k-bb含む全ツールのクリックが正確に計測できていない可能性がある。Issue 1の実施後に再計測する +2. **エラーによる離脱の可能性**: `calculate` 関数に `walks === 0` で `null` を返すロジックがあり、「与四球=0」と入力したユーザーが「入力値が正しくありません」エラーで計算完了できず離脱している可能性がある +3. **PVの少なさ**: 直帰率50%で実際に計算を実行しているユーザーが極めて少ない可能性がある + +#### タスク +- [ ] GA4またはSCで/tools/k-bbの実際のPVと流入クエリを確認する +- [ ] 「与四球=0」入力時のエラーメッセージを `"与四球が0の場合K/BBは∞(計算不能)です。1以上を入力してください"` に変更する(`calculator-definitions.ts` の k-bb エントリのバリデーションメッセージを改善するか、`CalculatorForm.tsx` のエラー文言を動的化する) +- [ ] Issue 1(data-cta属性付与)実施後に計測を開始する + +#### 受け入れ条件 +- [ ] 与四球=0入力時に分かりやすいエラーメッセージが表示される +- [ ] Issue 1実施後にk-bbのクリックが計測できる状態になっている + +#### 工数見積もり +1日(調査0.5日 + エラー文言修正0.5日) + +--- + +### Issue 4: [Add] /signup ページにアプリ優先誘導バナーと登録完了後アプリDL促進画面を追加する + +**本文**: + +#### 背景 +`/signup` ページのApp Store CTRが4%(月6件程度)と低い。「野球 個人成績 アプリ 無料」で検索して流入しているユーザーはアプリへの関心が高いが、Webの会員登録フォームに着地してしまい、アプリDLへの誘導が弱い。 + +#### 実装内容 + +**A. 登録フォーム上部にアプリ優先提案バナーを追加** +- `front/app/(app)/signup/page.tsx` の SignUpコンポーネントの上部にバナーを追加 +- Server Component として実装(`_components/AppSuggestionBanner.tsx`) +- 文言: 「iPhoneユーザーはアプリからの登録がスムーズです(30秒・完全無料)」 +- App Storeバッジリンク付き +- `data-cta="signup_page_app_suggestion"` を付与 + +**B. 登録完了後アプリDL誘導ページ `/signup/complete` を新設** +- 登録成功後に `/signup/complete` へリダイレクト +- App Store CTAを大きく表示し、「Webで続ける」を小リンクとして配置 +- `data-cta="signup_complete_main"` を付与 + +#### 受け入れ条件 +- [ ] /signup ページのSignUpフォーム上部にアプリ提案バナーが表示される +- [ ] Web登録完了後に /signup/complete へ遷移し、App Store CTAが表示される +- [ ] 「Webで続ける」リンクからトップページ(または/mypage)に戻れる +- [ ] Webフォームでの登録フローが壊れていない + +#### 工数見積もり +2日(バナー0.5日 + complete画面1.5日) + +--- + +### Issue 5: [Add] ランキングのWebデモページ `/ranking/demo` を新規作成する(未ログインユーザー向け) + +**本文**: + +#### 背景 +`/groups/[slug]` と `/mypage/[slug]` は認証必須のため、未ログインユーザーはランキングを一切見られない。サービスの価値を体験できないまま離脱しているユーザーが多い。モックデータを使ったデモページを作ることで、「自分も載りたい」というFOMO(機会損失恐怖)を訴求し、アプリ登録への動機を生む。 + +#### 実装内容 +- `front/app/(app)/ranking/demo/page.tsx` を新規作成(Server Component) +- `_data/mock-ranking.ts` にモックデータを定義(実ユーザーデータは使用しない) + - 打撃ランキング: 打率・本塁打・打点・安打・盗塁・出塁率の各上位5件 + - 投手ランキング: 防御率・勝利・奪三振の各上位5件 +- ランキングテーブルの上位3件は表示し、4位以降をぼかし(TailwindCSS `blur-sm`) +- ページ下部にApp Store CTAを大きく配置(`data-cta="ranking_demo_bottom"`) +- 「これはデモ表示です。実際のランキングはアプリ内で確認できます」という注記を入れる +- 構造化データ(WebPage)を付与 +- `/mypage/[slug]` の未ログイン時オーバーレイにApp Storeリンクボタンを追加 + +#### 受け入れ条件 +- [ ] `/ranking/demo` が未ログインユーザーでもアクセスできる +- [ ] モックデータによるランキングが表示され、4位以降がぼかし表示 +- [ ] App Store CTAが目立つ位置に配置されている +- [ ] モックデータであることが明記されている +- [ ] `/mypage/[slug]` の未ログイン時にApp Storeリンクが表示される + +#### 工数見積もり +2.5日 + diff --git a/docs/strategy/research/data-analysis-report.md b/docs/strategy/research/data-analysis-report.md deleted file mode 100644 index 2f9aa32..0000000 --- a/docs/strategy/research/data-analysis-report.md +++ /dev/null @@ -1,225 +0,0 @@ -# BUZZ BASE 実データ分析レポート - -**データ期間**: 2026年1月1日〜3月14日(73日間) -**データソース**: Google Analytics, Google Search Console - ---- - -## 1. トラフィック概要 - -| 指標 | 値 | -|------|-----| -| アクティブユーザー数(73日間) | 901 | -| 新規ユーザー数 | 876(97%が新規) | -| 平均エンゲージメント時間/ユーザー | 146.5秒(約2分27秒) | -| 総イベント数 | 12,836 | - -### 日別ユーザー推移の特筆事項 - -**直近の急成長**: データ後半(3月上旬〜)で新規ユーザーが急増。 - -| 期間 | 1日あたり新規ユーザー | 1日あたりリピーター | -|------|---------------------|-------------------| -| 1月前半(Day 0-14) | 平均 7.2人 | 平均 2.1人 | -| 2月前半(Day 30-44) | 平均 8.9人 | 平均 3.9人 | -| 3月上旬(Day 60-72) | **平均 28.5人** | **平均 9.3人** | - -**3月に入って新規ユーザーが3-4倍に増加している。** この成長要因の特定と持続が最優先。 - ---- - -## 2. 流入チャネル分析 - -### ユーザー獲得元(新規ユーザーベース) - -| チャネル | ユーザー数 | 割合 | 分析 | -|---------|-----------|------|------| -| **Google 検索** | 481 | **53.4%** | 最大の流入源。SEOが効いている | -| ダイレクト | 173 | 19.2% | ブックマーク、URL直接入力 | -| **Yahoo 検索** | 152 | **16.9%** | 無視できない規模。中高生のYahoo利用率を反映 | -| Bing 検索 | 20 | 2.2% | | -| runteq.jp | 17 | 1.9% | プログラミングスクール経由(開発者の認知) | -| ChatGPT | 16 | 1.8% | AIからの流入。新しいチャネル | -| GitHub | 15 | 1.7% | 開発者コミュニティ | -| **X(Twitter)** | 14 | **1.6%** | SNSからの流入は極めて少ない | -| Instagram | 1 | 0.1% | ほぼゼロ | - -### 重要な発見 - -1. **全体の72.5%がオーガニック検索**(Google + Yahoo + Bing)→ SEOが圧倒的な成長エンジン -2. **SNSからの流入はほぼゼロ**(X: 14人、Instagram: 1人)→ SNS未開拓の伸びしろが大きい -3. **ChatGPTからの流入が16人** → AI検索対応も今後重要に - ---- - -## 3. ページ別パフォーマンス分析 - -### 主要ページ(表示回数順) - -| ページ | 表示回数 | ユーザー | 直帰率 | 分析 | -|--------|---------|---------|--------|------| -| トップ/LP | 2,916 | 486 | 30.5% | 主要ランディングページ | -| LP(SEO版) | 417 | 241 | 35.6% | SEO用LP | -| **試合一覧** | 802 | 104 | 16.5% | アクティブユーザーのコア画面 | -| **試合結果まとめ** | 664 | 73 | **1.1%** | 非常に低い直帰率 = 高エンゲージメント | -| ログイン | 530 | 222 | 5.9% | | -| **打撃成績を記録** | 424 | 71 | 1.2% | コア機能。高エンゲージメント | -| **投手成績を記録** | 406 | 66 | 1.4% | コア機能。高エンゲージメント | -| グループ一覧 | 356 | 79 | 5.2% | | -| **新規会員登録** | 312 | 183 | 5.9% | | -| 野球ノート | 229 | 79 | 7.5% | | -| ダッシュボード | 203 | 35 | 7.5% | | -| ユーザー名登録 | 173 | 114 | 4.1% | | - -### 会員登録ファネル分析 - -``` -新規会員登録ページ: 183ユーザー - ↓ (62.3%) -ユーザー名登録: 114ユーザー - ↓ -アクティブ利用: 推定70-90ユーザー(MAU) -``` - -**新規登録の転換率は良好(62.3%)。** 問題は「登録後のアクティブ利用への定着」。 - -### 計算ツールページ - -| ツール | 表示回数 | ユーザー | 直帰率 | -|--------|---------|---------|--------| -| 成績計算方法&指標一覧 | 157 | 73 | 27.5% | -| OPS計算ツール | 127 | 82 | **43.6%** | -| 成績の算出方法 | 110 | 82 | 36.3% | -| 打率計算ツール | 65 | 33 | 2.0% | -| 出塁率計算ツール | 30 | 17 | 33.3% | -| 長打率計算ツール | 25 | 8 | 0% | -| K/BB計算ツール | 21 | 16 | 40.9% | -| 防御率計算ツール | 20 | 6 | 0% | -| WHIP計算ツール | 11 | 9 | 18.2% | -| K/9計算ツール | 4 | 4 | 20.0% | -| BB/9計算ツール | 2 | 2 | 50.0% | - -**計算ツールの合計**: 572表示回数、247ユニークユーザー - ---- - -## 4. Search Console分析 - -### 全体パフォーマンス - -| 指標 | 値 | -|------|-----| -| 総クリック数 | 591 | -| 総表示回数 | 18,848+ | -| 平均CTR | 約3.1% | -| デバイス比率 | モバイル 78% / PC 20% / タブレット 2% | - -### 高インプレッション・低CTRのキーワード(最大の機会) - -| クエリ | 表示回数 | クリック | CTR | 掲載順位 | 機会 | -|--------|---------|---------|-----|---------|------| -| **ops 計算** | **1,451** | 22 | 1.5% | 4.83 | CTR改善で大幅増加可能 | -| **打率計算** | **1,022** | 8 | 0.8% | 7.94 | 順位改善+CTR改善 | -| **長打率 計算** | **945** | 5 | 0.5% | 4.61 | CTR改善で大幅増加可能 | -| **出塁率 計算** | **465** | 9 | 1.9% | 3.71 | 上位表示済み、CTR改善 | -| **野球 個人成績 アプリ 無料** | **496** | 73 | 14.7% | 3.69 | 最重要キーワード。好調 | -| **打率計算アプリ** | 214 | 6 | 2.8% | 6.97 | 順位改善の余地 | -| k/bb | 173 | 1 | 0.6% | 5.68 | CTR改善 | -| ops計算 | 160 | 3 | 1.9% | 5.27 | | -| 野球成績アプリ | 155 | 4 | 2.6% | 5.86 | | -| ops 計算方法 | 131 | 1 | 0.8% | 7.04 | | - -### 上位クリックキーワード(既に効いているもの) - -| クエリ | クリック | CTR | 掲載順位 | -|--------|---------|-----|---------| -| 野球 個人成績 アプリ 無料 | **73** | 14.7% | 3.69 | -| buzz base | **31** | 46.3% | 1.61 | -| ops 計算 | **22** | 1.5% | 4.83 | -| buzzbase | **17** | 50.0% | 3.09 | -| 野球 成績 アプリ | **11** | 6.6% | 5.93 | - -### ページ別検索パフォーマンス - -| ページ | クリック | 表示回数 | CTR | 掲載順位 | -|--------|---------|---------|-----|---------| -| **buzzbase.jp/** | 373 | 4,928 | 7.6% | 4.78 | -| **/calculation-of-grades** | 115 | **8,129** | 1.4% | 5.47 | -| **/tools/ops** | 61 | **4,264** | 1.4% | 5.62 | -| /tools/k-bb | 16 | 720 | 2.2% | 5.11 | -| /tools/obp | 11 | 947 | 1.2% | 3.76 | -| /note/new | 10 | 187 | 5.4% | 8.67 | - -### 最重要の発見: SEOの巨大な成長余地 - -**`/calculation-of-grades` ページが表示回数8,129回でCTR 1.4%**。これは: -- CTRを5%に改善するだけで → 月間クリック+**約290件増** -- タイトル・メタディスクリプションの最適化だけで実現可能 - -**`/tools/ops` ページが表示回数4,264回でCTR 1.4%**。同様に: -- CTRを5%に改善 → 月間クリック+**約150件増** - -**計算ツール系のインプレッションは合計15,000回以上** → 適切なSEO最適化で数百件のクリック増が見込める。 - ---- - -## 5. 戦略への示唆 - -### 最優先アクション(データが示す最大の機会) - -#### 1. SEO最適化(最大ROI) - -現在の最大の成長エンジンはオーガニック検索(72.5%)。SNS施策よりも先に、既に効いているSEOを強化すべき。 - -**即効性の高い施策:** -- `/calculation-of-grades` のタイトル・メタディスクリプション改善(8,129インプレッションのCTR改善) -- `/tools/ops` のタイトル・メタディスクリプション改善(4,264インプレッション) -- 「打率計算」専用ページの強化(1,022インプレッションで掲載8位→上位狙い) -- 「長打率 計算」のCTR改善(945インプレッション、4.61位) -- 各計算ツールの構造化データ(FAQ, HowTo schema)追加 - -**中期施策:** -- 「防御率 計算」のコンテンツ強化(現在は表示回数0→圧倒的な検索ボリュームがある) -- 各指標の解説コンテンツ充実(「打率とは」「OPSとは」等の情報系クエリの取り込み) - -#### 2. 計算ツール → 会員登録の導線強化 - -計算ツールにはSEOで多くの非ログインユーザーが訪問している。**この訪問者を会員登録に誘導する導線**が最大の成長レバー。 - -- 計算ツール利用後に「成績を継続的に記録・管理しませんか?」のCTA -- 計算結果を保存するには会員登録が必要、という自然な誘導 -- 計算ツールページに成績カードのサンプル表示 - -#### 3. SNSマーケティング(未開拓チャネル) - -現在SNSからの流入はほぼゼロ。これは「やっていないだけ」であり、開始すれば純増が期待できる。 - -#### 4. 直近の急成長の要因分析 - -3月に入って新規ユーザーが3-4倍に増加している(日平均7→28人)。この要因を特定し、持続させることが重要。考えられる要因: -- 春季大会シーズンの開始 -- 何らかのSEO順位変動 -- 外部メディアでの言及 - -#### 5. リテンション改善 - -新規876人に対しMAU 80は、**リテンション率が極めて低い**(約9%)。計算ツール等の一時利用者を除いても、アプリ機能利用者の定着率向上が必要。 - ---- - -## 6. KPI修正提案 - -実データに基づき、以下のKPIを提案: - -| 指標 | 現状実績 | 3ヶ月目標 | 6ヶ月目標 | -|------|---------|----------|----------| -| 月間新規ユーザー(GA) | 約350人/月(3月ペース) | 500人/月 | 800人/月 | -| MAU(実アクティブ) | 80 | 200 | 500 | -| 検索クリック数/月 | 約240/月 | 400/月 | 700/月 | -| 計算ツールCTR | 1.4% | 3.0% | 5.0% | -| 新規登録転換率 | 62.3% | 65% | 70% | -| 7日リテンション | 未計測 | 30% | 40% | - ---- - -*本レポートは2026年1月1日〜3月14日のGoogle Analytics / Search Consoleデータに基づく分析です。* diff --git a/docs/strategy/research/ios-growth-strategy-202604.md b/docs/strategy/research/ios-growth-strategy-202604.md new file mode 100644 index 0000000..ac70a0a --- /dev/null +++ b/docs/strategy/research/ios-growth-strategy-202604.md @@ -0,0 +1,512 @@ +# BUZZ BASE iOS ダウンロード数増加のための市場調査・成長戦略レポート + +**作成日**: 2026年4月2日 +**目的**: BUZZ BASE iOSアプリのダウンロード数増加に向けた競合調査・市場分析・戦略立案 + +--- + +## 目次 + +1. 競合アプリ最新データ(2026年4月時点) +2. 中高生野球市場規模(最新統計) +3. 中高生のアプリ発見経路・利用動向 +4. App Store キーワード戦略(ASO) +5. 成功事例から学ぶ成長戦略 +6. BUZZ BASE のポジショニング提案 +7. 具体的アクションプラン + +--- + +## 1. 競合アプリ最新データ(2026年4月時点) + +### 1.1 主要競合アプリ一覧 + +| アプリ名 | プラットフォーム | 評価 | レビュー数 | 価格 | 主な対象 | +|---------|----------------|------|-----------|------|---------| +| 草野球日記 ベボレコ | iOS / Android | 4.8 | 4,323件 | 無料(広告削除¥300) | 草野球全般 | +| PLAY by TeamHub | iOS / Android | 4.5 | 4,377件 | 無料(月額¥2,000〜) | 草野球・少年野球 | +| ヒットメーカー | iOS | 4.8 | 1,809件 | 無料(広告あり) | アマ野球全般・少年野球 | +| スコアラー | iOS / Android | 4.5 | 779件 | 無料(¥1,800で5試合超) | アマ野球全般 | +| SCORE BASE | iOS / Android | 3.3 | 10件 | 無料(Pro¥6,600) | 少年〜社会人 | +| 球ログ 野球スコア | iOS | 4.4 | 435件 | 無料(課金あり) | アマ野球全般 | +| Nines(草野球応援アプリ) | iOS / Android | 未確認 | 少数 | 無料 | 草野球 | + +### 1.2 各アプリの詳細分析 + +#### 草野球日記 ベボレコ(最大の直接競合) + +**基本情報** +- リリース: 2012年頃(長期運営の老舗アプリ) +- レビュー数4,323件は競合中最多クラス → 高い信頼性と安定したユーザー基盤 +- 価格: 無料。広告削除・バックアップ各¥300のシンプルなマイクロ課金 + +**主な機能** +- 個人の打撃・投手成績記録(複数チーム参加対応) +- OPS・IsoD・IsoP・RC27・WHIP・FIPなど高度なセイバーメトリクス指標 +- 打球方向のスプレーチャート(視覚的分析) +- SNS連携シェア(Twitter・LINE・Evernote) +- 生涯成績の記録保管 + +**強み** +- 4.8という高評価と4,000件超のレビューによる高い信頼性 +- 「草野球」ターゲットの明確化でSEO・ASOで差別化 +- シンプルな個人成績管理に特化 +- 長期運用による安定性 + +**弱み・空白領域** +- チームを超えたランキング比較機能なし +- SNS的な「共有・競う」体験が弱い(シェアはできるが比較できない) +- 草野球(社会人)向けで、中高生向けの機能・UX設計ではない +- App Store レビューにログイン不具合の指摘が複数 + +**BUZZ BASEとの競合度**: 中(同じ個人成績管理だが対象ユーザーと提供価値が異なる) + +--- + +#### ヒットメーカー(打撃成績特化の直接競合) + +**基本情報** +- 評価4.8、レビュー1,809件 +- 公式発表:50,000ダウンロード突破(個人開発アプリとして注目の実績) +- 完全無料(広告収益モデル) + +**主な機能** +- 打撃成績の記録・OPS等自動計算 +- 期間別・試合形式別の成績分析 +- スイング数の記録(練習管理) +- グラフ・カレンダー表示 + +**強み** +- シンプルなUIで初心者でも使いやすい +- 「少年野球選手から親まで」の幅広いターゲット +- 個人開発で機敏な改善対応 + +**弱み・空白領域** +- 打撃のみ(投手成績機能なし) +- 他ユーザーとの成績比較・ランキング機能なし +- チーム機能なし +- 複数のレビューで「他ユーザーとの成績共有機能を求める声」が明確に存在 + +**BUZZ BASEとの競合度**: 高(個人成績管理でターゲットが重なる) + +--- + +#### PLAY by TeamHub(チーム管理の大手競合) + +**基本情報** +- 評価4.5、レビュー4,377件(3,189件が5つ星) +- TeamHub全体で100万ユーザー突破(2022年発表)、290,000チーム以上 +- 親会社 Link Sports(東京・スポーツテック企業) + +**主な機能** +- チームスコアの記録・共有 +- チーム管理(日程・出欠・連絡) +- 個人成績・チームランキングの自動集計 +- Plus:月額¥2,000、Elite:月額¥6,000 + +**強み** +- 圧倒的なユーザー数・チーム数 +- 野球だけでなくソフトボール対応 +- 月額課金によるSaaS的な安定収益 + +**弱み・空白領域** +- チーム横断のランキング比較ができない +- 中高生個人向けの設計より、チーム管理者(監督・コーチ)向け +- 月額¥2,000〜の有料機能は中高生には高額 + +**BUZZ BASEとの競合度**: 低〜中(チームvsユーザー個人の軸が異なる) + +--- + +### 1.3 競合マッピング(更新版) + +``` + 個人成績・SNS特化 + | + ヒットメーカー | BUZZ BASE ★ + 球ログ | (ランキング共有) + | + ─────────────────────┼───────────────────── + チーム管理中心 | 個人×競争・共有 + | + PLAY by TeamHub | (空白地帯) + スコアラー | + SCORE BASE | + | + チーム成績管理中心 +``` + +**BUZZ BASE の空白地帯**: 「個人成績 x ランキング比較 x グループ共有」という軸で直接競合するアプリは2026年4月時点で存在しない。 + +--- + +## 2. 中高生野球市場規模(最新統計) + +### 2.1 高校野球人口(2025年) + +日本高等学校野球連盟 公式データ(2025年5月末現在) + +| 指標 | 数値 | +|------|------| +| 加盟校数 | 3,768校 | +| 総部員数 | 125,381人 | +| 1年生 | 43,312人 | +| 2年生 | 41,227人 | +| 3年生 | 40,842人 | +| 平均部員数/校 | 33.3人 | +| 継続率 | 90.1% | + +- 10年前(2015年)比で約15%減少 +- ただし継続率90.1%は高水準を維持 + +### 2.2 中学野球人口(2023〜2024年) + +中体連データおよび各種公開情報に基づく推計 + +| カテゴリ | 人数(推計) | +|---------|------------| +| 中学軟式野球部員(中体連) | 約133,725人(2023年度) | +| 中学硬式野球(リトルシニア・ボーイズ等) | 推定3〜5万人 | +| 女子野球部員(中学・高校) | 約6,135人(増加傾向) | +| **中高生野球人口合計(推計)** | **約28〜32万人** | + +**重要なトレンド**: +- 中学軟式は2001年(322,529人)の41%まで激減 +- 高校野球は相対的に安定(中学比で高校への進学後も継続率が高い) +- 中学硬式クラブチームは少子化比で相対安定(親の投資意欲) + +### 2.3 ターゲット市場規模(TAM/SAM/SOM) + +| レベル | 定義 | 推定人数 | +|--------|------|---------| +| TAM | 日本の中高生野球人口全体 | 約28〜32万人 | +| SAM | スマートフォン所持 + デジタルツール利用に積極的 | 約18〜22万人 | +| SOM(3年目標) | BUZZ BASE が現実的に獲得可能なユーザー | 約5,000〜1万人 | + +- 現在のMAU 838人は SAMの0.4〜0.5% の浸透率 +- ベボレコの推定ユーザー(DL2万以上)でも浸透率約10% +- **成長余地は大きい** + +--- + +## 3. 中高生のアプリ発見経路・利用動向 + +### 3.1 中高生のSNS利用実態(2024年調査) + +TesTee Lab・スタディプラス等の調査より + +| SNS | 中学生利用率 | 高校生利用率 | 特徴 | +|-----|------------|------------|------| +| LINE | 最高(ほぼ全員) | 最高 | 連絡・グループ利用 | +| TikTok | 2位(中学生) | 高い | 発見・エンタメ | +| Instagram | 高い | 2位(高校生) | 検索・情報収集 | +| X(旧Twitter) | 中程度 | 高い | 情報収集・口コミ | +| YouTube | 非常に高い | 非常に高い | 動画コンテンツ | + +**重要な発見**: +- 中学生はTikTokが情報発見の主要チャネル +- 高校生はInstagramで検索・情報収集する傾向(Google代わり) +- SNSで検索した商品・サービスを実際に購入・利用した男性の第1位は「スマホアプリ」(33.2%) +- Z世代は「実際の動画・写真ベース」の情報を信頼する + +### 3.2 アプリ発見経路の推定優先順位(中高生野球部員) + +1. **口コミ・チームメイトの紹介**(最も強い) + - 部活の仲間が使っているのを見て → 「俺も入れる」 + - チームで一斉導入(監督・主将の号令) + - これが最も低コストで最も効果的な成長ドライバー + +2. **App Store 検索**(重要) + - 「野球 成績」「野球 打率」「野球 スコア」等で検索 + - 検索結果上位表示 = 安定した自然流入 + - キーワード評価数・レビュー評価がランキングに影響 + +3. **SNS(TikTok・Instagram)**(成長加速に有効) + - 「野球部あるある」「成績管理してみた」系の動画 + - TikTok:拡散力高いが再現性の確保が難しい + - Instagram:部活系アカウントへのリーチ + +4. **Web検索からアプリへの誘導**(現状の主力チャネル → 活用余地大) + - 現在のWeb MAU 838人がいる → この人たちにアプリを訴求 + - 「ops 計算」「打率計算」でWebに来た人をアプリDLに転換 + +5. **レビューサイト・まとめ記事** + - 「野球アプリ おすすめ」等の記事での掲載 + - 複数の比較サイトへの掲載実績が流入を安定化 + +### 3.3 アプリを継続利用する条件(中高生の特性) + +- **すぐに使える**: 登録・初期設定の手間が少ない +- **視覚的に楽しい**: データがグラフ・ビジュアルで見える +- **友達も使っている**: ソーシャル要素(一人では使いにくい) +- **通知がある**: プッシュ通知で定期的にリマインド +- **無料**: 中高生は課金ハードルが高い(保護者管理のケースも多い) + +--- + +## 4. App Store キーワード戦略(ASO) + +### 4.1 競合が使っているキーワード分析 + +主要競合のApp Storeページで使われているキーワードを分析。 + +| キーワード | 使用競合数 | 検索ニーズ推定 | +|-----------|----------|--------------| +| 野球 | 全競合 | 非常に高い | +| スコア | 多数 | 高い | +| 成績 | 多数 | 高い | +| 打率 | 多数 | 高い | +| 防御率 | 一部 | 中程度 | +| OPS | 一部 | 中程度(認知拡大中) | +| WHIP | 一部 | 低〜中 | +| セイバーメトリクス | 一部 | 低(ニッチ) | +| ランキング | ほぼなし | 中(BUZZ BASE独自の機会) | +| 成績比較 | なし | 低〜中(BUZZ BASE独自の機会) | +| グループ | なし | 低(BUZZ BASE独自の機会) | + +### 4.2 BUZZ BASE 推奨 ASOキーワード + +**App Storeタイトル(30文字以内)** +``` +BUZZ BASE - 野球成績・ランキング管理 +``` + +**サブタイトル(30文字以内)** +``` +打率・OPS・防御率を仲間と比較 +``` + +**キーワードフィールド(100文字以内)に含めるべき語句** +``` +野球,成績,打率,防御率,OPS,ランキング,スコア,個人成績,高校野球,中学野球,セイバーメトリクス,WHIP,FIP,記録,管理 +``` + +**プロモーションテキスト(170文字以内)の方向性** +- BUZZ BASEだけが持つ「ランキング比較」機能を前面に +- 「仲間と競う」体験価値を訴求 +- 無料であることを明示 +- 中高生向けであることを明示 + +**候補テキスト例**: +``` +野球の個人成績を記録して、仲間とランキングで競い合おう。打率・防御率・OPSなど29種類の指標を自動計算。グループを作ってチーム横断の成績比較ができる、中高生野球部員のための無料アプリ。 +``` + +### 4.3 競合との検索キーワード棲み分け + +| キーワード | 競合 | BUZZ BASE の戦略 | +|-----------|------|-----------------| +| 野球 スコア | スコアラー・TeamHub | 共存(スコア記録機能もあると訴求) | +| 野球 成績 | ヒットメーカー・ベボレコ | 直接競合。ランキング差別化で共存 | +| 野球 記録 | 全競合 | 共存 | +| 野球 ランキング | 競合なし | **独占可能な重要キーワード** | +| 野球 成績 比較 | 競合なし | **独占可能なキーワード** | +| 高校野球 成績 管理 | 少数 | **BUZZ BASE独自の訴求点** | +| 中学野球 アプリ | ほぼなし | **BUZZ BASE独自の訴求点** | + +--- + +## 5. 成功事例から学ぶ成長戦略 + +### 5.1 ヒットメーカー(個人開発、50,000DL達成)の成功要因分析 + +野球個人成績管理という同カテゴリで50,000DLを達成した個人開発アプリの分析。 + +**成功要因推定**: +1. **シンプルなUX**: 入力画面を徹底的にシンプルに絞り込み +2. **「打撃」に絞り込む**: 投手・守備を捨てて打者に特化 → ターゲットの解像度向上 +3. **高い評価維持**: 4.8という評価を維持することでApp Store内の信頼性確保 +4. **継続的なアップデート**: ユーザーレビューへの返答・改善対応 +5. **無料維持**: 広告収益モデルで課金ハードルゼロ + +**BUZZ BASEへの示唆**: +- レビュー数1,809件に対しBUZZ BASEのiOSはリリース直後 → 早期の高評価レビュー獲得が最優先 +- 「ランキング機能欲しい」というレビューがヒットメーカーにも存在 → BUZZ BASEへの移行訴求余地 + +### 5.2 草野球日記 ベボレコ(4,323件レビュー)の長期成長分析 + +**成功要因推定**: +1. **長期運用**: 2012年頃から継続。累積レビューが信頼性を形成 +2. **「草野球」ブランディング**: タイトルにターゲットを明示 → 検索マッチング最適化 +3. **マイクロ課金設計**: ¥300の広告削除は中高生でも許容しやすい価格設定 +4. **口コミ・チーム紹介**: 草野球チームメンバーへのバイラル普及 +5. **安定性**: 長期間の運用で「安心して使えるアプリ」の印象 + +**BUZZ BASEへの示唆**: +- タイトル・キーワードにターゲット(中高生・高校野球・中学野球)を明示する +- 安定性・継続性のシグナルを送る(定期アップデート、レビュー返答) + +### 5.3 PLAY by TeamHub(100万ユーザー)の成長戦略 + +**成功要因**: +1. **B2B的なチーム単位での普及**: 一人が導入するとチーム全員が使う構造 +2. **幅広い競技対応**: 107種目で市場を最大化 +3. **フリーミアム設計**: 無料で十分使えて、有料へのアップグレードが自然 +4. **メディア掲載・パートナーシップ**: ゼビオグループとの提携 + +**BUZZ BASEへの示唆**: +- **チームへの一斉導入** を促す仕組みが重要(招待機能・紹介特典の強化) +- 野球部の主将・副主将に「チームで導入してもらう」ターゲティング + +### 5.4 Youth Sports アプリ成功の共通パターン(グローバル事例) + +スポーツアプリ市場は年成長率11.7%(2025年〜2030年予測)。成功アプリの共通点: + +1. **バイラル係数を高める設計**: 「友達を誘わないと使えない機能」の設置 +2. **コンテンツのSNS共有性**: 成績をスクリーンショット・画像で共有しやすい形式 +3. **スポーツシーズンに合わせた施策**: 春(4月)・夏(7〜8月)の試合時期に集中投下 +4. **App Store レビュー戦略**: アプリ内で適切なタイミングでレビュー依頼 + +--- + +## 6. BUZZ BASE のポジショニング提案 + +### 6.1 現在のポジション + +- 「個人成績記録」は競合多数の領域 +- **「仲間とランキードで競い合う」は競合ゼロの空白地帯** +- Web版のSEO流入(月838人)という独自の強みを持つ + +### 6.2 推奨ポジション + +**「中高生野球部員の成績SNS」** + +競合は「記録ツール」として位置づけているのに対し、BUZZ BASEは「競争・共有の体験」として位置づける。 + +``` +ポジショニングステートメント: +「野球をやっている中高生のための、個人成績を記録して仲間とランキードで競い合える唯一のアプリ」 +``` + +### 6.3 差別化マトリクス + +| 機能・特性 | ベボレコ | ヒットメーカー | TeamHub | BUZZ BASE | +|-----------|---------|--------------|---------|-----------| +| 個人成績記録 | あり | あり(打撃のみ) | チーム経由 | **あり(打撃+投手29種)** | +| 友達とランキング比較 | なし | なし | チーム内のみ | **あり(グループ横断)** | +| 中高生特化 | なし(草野球向け) | なし(全年代) | なし(全年代) | **あり** | +| チーム横断グループ | なし | なし | なし | **あり** | +| 無料 | 基本無料 | 完全無料 | 基本無料 | **完全無料** | +| iOS アプリ | あり | あり | あり | **開発中(訴求点)** | +| Web版 | なし | なし | なし | **あり(SEO流入済み)** | + +### 6.4 ターゲットペルソナ + +**メインターゲット: 高校野球部員(15〜18歳)** +- 毎週試合がある(春・秋のリーグ戦、夏の大会) +- 成績を意識して練習している(打率・防御率を自分で計算している) +- スマートフォン所持率ほぼ100% +- TikTok・Instagram・X を利用 +- 自分の成績をチームメイトと比べたい気持ちがある + +**サブターゲット: 中学野球部員(12〜15歳)または硬式クラブチーム所属** +- 親がスマートフォンを管理するケースもあり +- 高校進学に向けて成績アピールのニーズ +- ボーイズリーグ・シニアリーグなどクラブチームはデータ活用意識が高い + +--- + +## 7. 具体的アクションプラン + +### 7.1 App Store 最適化(ASO)- 即座に実施すべき施策 + +**優先度: 最高** + +| 施策 | 内容 | 工数見積 | +|------|------|---------| +| スクリーンショット最適化 | ランキング画面・成績入力画面を見せる。「ランキード」「29種の成績」を文字で強調 | 小 | +| アプリ説明文の改善 | 上記キーワード戦略を反映。差別化ポイントを冒頭3行に集約 | 小 | +| プロモーションテキスト | 「仲間と競い合う」体験価値を30文字以内で | 小 | +| キーワードフィールド最適化 | 競合が使っていない「ランキング」「成績比較」を追加 | 小 | +| App Preview動画 | 成績入力→ランキング表示の流れを30秒動画で見せる | 中 | + +### 7.2 口コミ・バイラル設計 - 最も重要な成長ドライバー + +**優先度: 最高** + +中高生の野球アプリは「チームに広まる」ことが最大の成長メカニズム。 + +| 施策 | 内容 | 期待効果 | +|------|------|---------| +| 招待機能の強化 | 「友達をグループに招待」のフローをUXの中心に | チーム単位での普及 | +| 成績シェア画像生成 | Instagram・Xに貼りやすいランキング画像をワンタップ生成 | SNS上での自然な口コミ | +| チーム作成のウィザード | 「5人以上でグループを作ろう」という導線を強化 | 離脱防止 + バイラル | +| 紹介特典(非課金) | 友達3人招待で特定機能開放など | 招待モチベーション | + +### 7.3 Web版 → iOS アプリへの誘導強化 + +**優先度: 高** + +現在のWeb MAU 838人をiOSアプリへ誘導することが最短で結果を出せる施策。 + +| 施策 | 内容 | 期待DL数 | +|------|------|---------| +| App Storeバナー設置 | Web版の全ページにiOSアプリダウンロードバナーを設置 | 50〜100 | +| 計算ツールページ誘導 | SEO流入の主力ページ(OPS計算、打率計算)にCTA追加 | 100〜200 | +| Smart App Banner | `<meta name="apple-itunes-app">` タグでiOSデバイスから自動バナー表示 | 30〜80 | +| 登録後のオンボーディング | Web版新規登録ユーザーに「iOSアプリはこちら」のメール/通知 | 20〜50 | + +**試算**: Webからの転換率5〜10%でMAU838人 → 月40〜80DL + +### 7.4 SNS施策 - 中期(3〜6ヶ月)での認知拡大 + +**優先度: 中** + +| チャネル | 施策内容 | ターゲット | +|---------|---------|-----------| +| TikTok | 「野球部の成績管理してみた」「打率計算がラクになった」系の動画。アカウント運営またはユーザーに投稿依頼 | 中学生 | +| Instagram | 野球部系アカウント(部活紹介)への投稿。リール動画でランキング画面を見せる | 高校生 | +| X(Twitter) | 「野球部あるある」ハッシュタグへの参加。成績ランキングのスクリーンショット文化の形成 | 高校生〜大学生 | + +**重要な視点**: 個人開発者として「作り手の物語」を発信することで共感を集めやすい。開発日記・ユーザーの声の紹介などのコンテンツが中高生にウケる可能性。 + +### 7.5 レビュー獲得戦略 + +**優先度: 高** + +競合は数百〜数千件のレビューを保有。BUZZ BASEのiOSはリリース後すぐにレビュー獲得が必要。 + +| タイミング | 施策 | +|-----------|------| +| 初回ログイン後3日目 | アプリ内でレビュー依頼(`SKStoreReviewRequest`) | +| グループに5人以上が集まった時 | 「このアプリが気に入ったらレビューをお願いします」 | +| シーズン終了時(大会後) | 「今シーズンの成績まとめができました。レビューいただけますか?」 | + +**目標**: リリース後3ヶ月で100件、6ヶ月で300件のレビュー取得 + +--- + +## 付記: MAU別の成長ロードマップ + +| フェーズ | MAU目標 | 主な施策 | 収益化 | +|---------|--------|---------|-------| +| Phase 1(現在〜6ヶ月) | 838 → 2,000 | ASO最適化、Web→アプリ転換、口コミ設計 | 広告(バナー) | +| Phase 2(6〜12ヶ月) | 2,000 → 5,000 | SNS施策、チーム導入促進、レビュー強化 | 広告収益安定化 | +| Phase 3(1年〜2年) | 5,000 → 15,000 | 口コミバイラル、メディア掲載、機能拡充 | プレミアム機能検討 | + +浸透率換算: MAU 15,000 = SAM(約18〜22万人)の約7%。ベボレコ相当の規模。 + +--- + +## 調査ソース + +- [App Store - ヒットメーカー](https://apps.apple.com/jp/app/%E3%83%92%E3%83%83%E3%83%88%E3%83%A1%E3%83%BC%E3%82%AB%E3%83%BC/id1634614073) +- [App Store - 草野球日記 ベボレコ](https://apps.apple.com/jp/app/%E8%8D%89%E9%87%8E%E7%90%83%E6%97%A5%E8%A8%98-%E3%83%99%E3%83%9C%E3%83%AC%E3%82%B3/id578136103) +- [App Store - PLAY by TeamHub](https://apps.apple.com/jp/app/play-by-teamhub-%E9%87%8E%E7%90%83%E3%81%AE%E3%82%B3%E3%82%A2%E7%AE%A1%E7%90%86/id1298937736) +- [App Store - スコアラー](https://apps.apple.com/jp/app/%E3%82%B9%E3%82%B3%E3%82%A2%E3%83%A9%E3%83%BC/id1522649930) +- [App Store - 球ログ 野球スコア](https://apps.apple.com/jp/app/%E7%90%83%E3%83%AD%E3%82%B0-%E9%87%8E%E7%90%83%E3%82%B9%E3%82%B3%E3%82%A2/id1447656926) +- [日本高等学校野球連盟 - 部員数統計](https://www.jhbf.or.jp/data/) +- [笹川スポーツ財団 - 10代の野球人口](https://www.ssf.or.jp/thinktank/sports_life/data/baseball_teens.html) +- [TesTee Lab - 学生のSNS利用に関する調査 2024年版](https://lab.testee.co/sns_student2024/) +- [Full-Count - 野球離れに加速感](https://full-count.jp/2025/03/04/post1710012/) +- [Nines 公式サイト](https://ninesbb.com/) +- [SCORE BASE 公式サイト](https://scorebase.net/) +- [PLAY by TeamHub 公式サイト](https://tmhub.jp/play/) +- [アプリブ - 野球スコアアプリおすすめ6選](https://app-liv.jp/sports/all/1298/) +- [スポーツアプリ市場レポート](https://www.gii.co.jp/report/tbrc1989702-sport-app-global-market-report.html) +- [GII - スポーツアプリグローバル市場](https://www.gii.co.jp/report/tbrc1989702-sport-app-global-market-report.html) + +--- + +*本レポートは公開情報・App Store掲載情報・各種調査データに基づく調査結果です。一部推定値を含みます。* +*作成: BUZZ BASE 市場調査エージェント(2026年4月2日)* diff --git a/docs/strategy/web-to-app-master-plan-202604.md b/docs/strategy/web-to-app-master-plan-202604.md new file mode 100644 index 0000000..aafc0c6 --- /dev/null +++ b/docs/strategy/web-to-app-master-plan-202604.md @@ -0,0 +1,313 @@ +# Web → iOSアプリ登録数 統合マスタープラン (2026-04) + +作成日: 2026-04-30 +作成者: 戦略リーダー (BUZZ BASE Revenue Strategy Lead) +対象期間: 2026-05〜2026-07 (3ヶ月) / 中期目標: 2026-10末 +目的: **Webからの流入を起点に、iOSアプリの月間新規登録数を最大化する** + +--- + +## 0. 本ドキュメントの位置づけ + +- 4/2公開の前回統合レポート [`ios-app-download-growth-integrated-202604.md`](./ios-app-download-growth-integrated-202604.md) で打ち出した「SmartAppBanner / CalculatorFormCTA / App Storeメタデータ最適化」は **すべて実装済み**。 +- それでも月間DLが想定より伸びていないため、本マスタープランで **次の打ち手** に踏み込む。 +- 並行で3エージェント(analytics-analyst / product-planner / growth-marketing)が個別の領域別プランを作成中。本ドキュメントはそれらを統合する **上位戦略**。 + - 各エージェントの出力ファイル(後追い統合): + - `docs/strategy/measurement-plan-web-to-app-202604.md` + - `docs/strategy/product/web-to-app-conversion-202604.md` + - `docs/strategy/marketing/web-traffic-to-app-202604.md` +- 独立してこのマスタープランだけを読んでも、5月の最初の2週間で何をやるかが分かる構造になっている。 + +--- + +## 1. 現状サマリー (2026-04-01〜2026-04-29) + +### 数字 +| 指標 | 値 | 備考 | +|------|----|------| +| 総アクティブユーザー | 1,162 | 前年同月比 +約40% | +| セッション | 1,723 | | +| 新規率 | 94.6% | リピート率の低さが課題 | +| App Store発信クリック | 約85件/月 | 粗いproxy指標、CV未設定 | +| TOPページ App Store CTR | 11% | 健全 | +| tools系 App Store CTR | 1〜3% | **要改善** | +| /tools/k-bb CTA CTR | 0% | **CTA不具合の疑い** | +| SC quick-wins ポテンシャル | +700クリック | タイトル・ディスクリプション最適化 | + +### 流入構成 +- Google 50% / Yahoo 22% / Direct 17% / Bing 9% / SNS 0.3% +- AI検索(ChatGPT, Perplexity, Gemini)からの流入が **4月に出始めた**(新シグナル) + +### 構造的課題(仮説) +1. **計測の盲点**: 「Webクリック → App Store表示 → DL → アカウント作成」の各ファネルが断絶している。 +2. **tools系のCTA設計**: TOPは11%出ているのに、tools系が1〜3%。**ユーザー文脈に合っていない可能性**。 +3. **/tools/k-bb のCTA 0%**: 計測バグ or CTA表示バグの可能性。即調査すべき。 +4. **SNS 0.3%の異常値**: ターゲット中高生は X/Instagram/TikTok中心のはず。**未開拓チャネル**。 +5. **AI検索流入**: 新しい上流。LLMが拾いやすい構造化コンテンツへの先行投資価値あり。 + +--- + +## 2. North Star Metric と KPIツリー + +### North Star Metric (NSM) +**月間アプリ新規登録数(推定)** + +直接計測は困難なので、以下のproxy指標の組み合わせで推定する: +- App Store Connect Analytics の「Page Views」「First-Time Downloads」(月次) +- Webからの App Store発信クリック数 × 推定DL率 × 推定登録率 + +### KPIツリー + +``` +[NSM] 月間アプリ新規登録数(推定) + │ + ├── [L1-A] Web → App Storeクリック数 ← 我々が直接コントロールできる主戦場 + │ │ + │ ├── [L2-A1] SEO流入セッション数 + │ │ ├── [L3] 表示回数(インプレッション) + │ │ ├── [L3] 検索順位 + │ │ └── [L3] SEO CTR(タイトル・ディスクリプション) + │ │ + │ ├── [L2-A2] tools系ページ → CTAクリック率(現状 1〜3% / 目標 5〜8%) + │ │ ├── [L3] CTA表示位置(結果直後 / フッター / SmartAppBanner) + │ │ ├── [L3] CTAコピー(結果値の動的表示) + │ │ └── [L3] CTA表示率(現状 /tools/k-bb で 0% の不具合疑い) + │ │ + │ ├── [L2-A3] / (TOP) → CTAクリック率(現状 11% / 維持) + │ │ + │ ├── [L2-A4] /signup → app_store_click 率(ほぼ未計測) + │ │ └── [L3] Web会員登録完了画面でアプリDL誘導 + │ │ + │ └── [L2-A5] SNS / AI検索 流入セッション数(新規開拓) + │ ├── [L3] X 公式アカウント運用 + │ └── [L3] LLM最適化(FAQ / 構造化データ) + │ + ├── [L1-B] App Storeページ → DL率 + │ │ + │ ├── [L2-B1] App Storeメタデータ(実装済み、継続テスト) + │ ├── [L2-B2] スクリーンショット A/B(未着手) + │ └── [L2-B3] レビュー数(現状少 / 目標 25件@7月末) + │ + └── [L1-C] DL → アカウント作成率 + │ + ├── [L2-C1] アプリ初回起動の摩擦(オンボーディング) + └── [L2-C2] サインアップフォームの離脱率 +``` + +### KPI担当領域マッピング +| KPI階層 | 主担当エージェント | +|---------|------------------| +| L1-A 全体 | growth-marketing + product-planner | +| L2-A1 SEO流入 | growth-marketing | +| L2-A2 tools系CTA | product-planner | +| L2-A4 /signup→app誘導 | product-planner | +| L2-A5 SNS / AI検索 | growth-marketing | +| L1-B App Store | growth-marketing (ASO) | +| 全KPI 計測実装 | analytics-analyst | + +--- + +## 3. 3ヶ月実行ロードマップ + +> **大原則**: 週10〜15時間の個人開発工数。**5月は「計測 × 不具合修正 × Quick Win」に全振り**。新機能開発は6月後半以降。 + +### 5月(計測整備 + 漏れバケツの穴塞ぎ)目標DL: +60〜80 +**目玉施策3つ** +1. **GA4ファネル計測の本格実装**(analytics-analyst のプランに従う) + - `app_store_click` イベントを全CTAで統一 + - tools系ページ別のCTRをセグメント計測 + - /signup 完了後の `app_store_click` をクロスセル指標化 +2. **/tools/k-bb のCTA不具合調査・修正** + - 計測実装の最初のチェックで原因特定(CTA非表示 or イベント未発火) + - 同型バグが他tools系に潜んでいないかも横断確認 +3. **SC quick-win タイトル・ディスクリプション最適化**(+700クリック相当のポテンシャル回収) + - 既存記事のmetaタグだけ書き換え、新規執筆は最小限 + +**何を捨てるか** +- アプリ側の新機能開発(招待URL、シェア機能)→ 6月後半以降に延期 +- AdMob導入 → 7月以降(DLが伸びる前にマネタイズしても意味が薄い) +- Instagram運用 → 7月以降(X一本に集中) + +**5月の最初の2週間で具体的にやること** +| 日付目安 | タスク | 工数 | 担当領域 | +|---------|------|------|---------| +| 5/1〜5/3 | analytics-analyst の計測プランをレビュー、`app_store_click` を全CTAに実装 | 半日〜1日 | analytics | +| 5/4 | GA4 デバッグビューで全CTAのイベント発火確認 | 1時間 | analytics | +| 5/4〜5/5 | /tools/k-bb の CTA表示状況を実機確認、原因特定 | 1〜2時間 | product | +| 5/5 | /tools/k-bb 修正リリース | 1〜2時間 | product | +| 5/6〜5/8 | SC quick-win 記事10件のmetaタグ書き換え | 半日 | growth-marketing | +| 5/9〜5/10 | tools系CTAのABテスト準備(コピー2案を用意) | 半日 | product | +| 5/11〜5/14 | tools系CTA ABテスト開始(最低7日回す) | 30分/日モニタリング | product + analytics | + +### 6月(CTA最適化 + SNS本格運用)目標DL: +100〜130 +**目玉施策3つ** +1. **tools系CTA最適化(5月のABテスト結果を反映)** + - 勝ちパターンを全tools系に横展開 + - 結果値の動的表示・スクリーンショット差し込み等 +2. **X 公式アカウントの本格運用** + - 週3〜5投稿、成績Tips系コンテンツ + - 計算ツールの結果シェアテンプレを提供(バイラル設計) +3. **/signup 完了後のアプリ誘導画面追加** + - Web登録ユーザー(月170人)への確実なDL誘導 + - 「アプリでこの成績の推移をグラフで見よう」訴求 + +**何を捨てるか** +- 新規SEO記事の量産は止める(5月のSC最適化で十分な伸びがあれば) +- Instagram / TikTok は7月以降 +- グループ招待URL機能は6月後半に着手 → リリースは7月 + +### 7月(夏の大会シーズン × 拡大)目標DL: +150〜200 +**目玉施策3つ** +1. **グループ招待URL機能リリース**(チーム単位での口コミ普及) +2. **App Storeスクリーンショット ABテスト**(DL率改善) +3. **AI検索(LLM)最適化** + - 「中学野球 OPS とは」等のクエリでLLMに引用される構造化コンテンツ + - FAQ schema、明確な定義文、引用しやすい段落構成 + +**何を捨てるか** +- 新たな計算ツール追加は8月以降 +- AdMob は8月(DLベースが整ってから) + +--- + +## 4. 優先度マトリクス(効果 × 工数) + +> 効果は「月間DL推定 +X」、工数は実装〜検証まで。 + +| 工数↓ / 効果→ | 高 (+50DL/月以上) | 中 (+15〜50DL/月) | 低 (+15DL未満) | +|---------------|-------------------|-------------------|----------------| +| **30分** | **[QW] /tools/k-bb CTA不具合修正** | SmartAppBannerの dismiss仕様の `app-argument` 調整 | TOPページ CTA文言微調整 | +| **半日** | **[QW] GA4 `app_store_click` 統一実装**, **[QW] SC quick-win メタタグ最適化10記事** | tools系CTAコピーABテスト準備 | App Storeリンクに UTM 付与 | +| **1日** | **tools系CTA ABテスト本実装** | /signup完了画面のアプリ誘導 | Xアカウント開設・初期投稿 | +| **数日** | **tools系CTAの結果値動的表示** | スクリーンショット差し替え | LLM向けFAQ schema実装 | +| **週単位** | グループ招待URL機能 | アプリ初回オンボーディング改善 | TikTok運用 | + +**Quick Win (QW) の実行順序(5月最初の2週間で全部やる)** +1. /tools/k-bb CTA不具合修正(30分、効果: 単独で +5〜15DL/月、副次的に他tools系の同型バグ発見の可能性) +2. GA4 `app_store_click` 統一実装(半日、効果: ここから全施策の効果計測が可能になるので**全施策の前提**) +3. SC quick-win メタタグ最適化10記事(半日、効果: 月+700クリックの一部回収で +20〜40DL/月) + +--- + +## 5. PDCAサイクル設計 + +### 週次レビュー指標(最大5つ) +1. **Web → App Storeクリック数**(前週比) +2. **tools系ページ別 CTR**(特に /tools/k-bb と OPS計算) +3. **SEO流入セッション数**(Google + Yahoo + Bing) +4. **/signup 完了後の `app_store_click` 率** +5. **App Store Connect の First-Time Downloads**(取得可能な場合のみ) + +毎週月曜の朝30分でGA4 + Search Console + ASCを横断チェック。 + +### 月次レビュー指標 +- 上記5指標 + 以下を追加 + - 月間アプリ新規登録数(推定) + - X 公式アカウントのインプレッション・フォロワー増加数 + - レビュー獲得数 + - AI検索流入の有無・量 +- 月初に前月の結果サマリーを `docs/strategy/pdca/` 配下にレポート化。 + +### 計測実装後 最初のCheckポイント(5月中旬目安) +**成功基準**: +- 全CTAの `app_store_click` が GA4 デバッグビューで発火確認できている +- tools系ページ別のCTRが計測できている +- /tools/k-bb のCTAが正常動作している(CTR > 0%) +- /signup → app_store_click の遷移率が初めて可視化できている + +これが達成されていれば、5月後半〜6月のCTA最適化施策に進めるGOサイン。 + +### Plan-Do-Check-Actサイクル運用 +| フェーズ | 実施タイミング | 所要時間 | +|---------|--------------|---------| +| Plan | 月初 | 半日 | +| Do | 月中 | 通常実装 | +| Check | 毎週月曜 + 月末 | 30分/週 + 1時間/月 | +| Act | 月末 | 1時間 | + +--- + +## 6. KPI目標値 + +| 指標 | 4月実績 | 5月末 | 6月末 | 7月末 | 10月末 | +|------|--------|-------|-------|-------|--------| +| 月間アプリDL推定 | ~50 | 110 | 230 | 400 | 800 | +| App Storeクリック数 | 85 | 200 | 400 | 650 | 1,200 | +| Webセッション数 | 1,723 | 2,000 | 2,400 | 3,000 | 4,500 | +| SEO流入CTR(平均) | 1.8% | 2.5% | 3.2% | 3.8% | 4.5% | +| tools系→CTAクリック率 | 1〜3% | 4% | 6% | 8% | 10% | +| TOP→CTAクリック率 | 11% | 11% | 12% | 13% | 14% | +| Xフォロワー | 0 | 30 | 100 | 250 | 700 | +| App Storeレビュー | 0 | 5 | 15 | 30 | 60 | + +**計算根拠** +- 5月: tools系CTA改善 + SC quick-win = +60〜80DL想定 → 累積110 +- 6月: SNS流入立ち上がり + signup後誘導 = +100〜130DL想定 → 累積230 +- 7月: 招待URL + 大会シーズン = +150〜200DL想定 → 累積400(ただし夏は中高生が大会で忙しいので登録は鈍化リスクあり、保守側に倒した数字) +- 10月: 新チーム始動シーズン × 累積効果 = 月800(中学新チーム8月、高校新チーム7月のため、10月は安定期) + +--- + +## 7. リスクと緩和策 + +### R1. SmartAppBanner の dismiss後7日間表示されない仕様 +- **影響**: 一度閉じたユーザーには再露出のチャンスがない。リピーター(5%程度)には効かない。 +- **緩和策**: + - SmartAppBannerに**過度に依存しない設計**(CTAをコンテンツ内・フッターにも配置) + - ファーストビュー上のインライン誘導をtools系結果直後に強化 + - Web/SP共通でdismiss後でも表示されるカスタムバナー(控えめなフッター固定)の検討(ただし二重露出になるとUX悪化なので慎重に) + +### R2. iOS Safariネイティブ apple-itunes-app バナーと SmartAppBanner の二重表示問題 +- **影響**: ユーザーには同じバナーが2つ見える事故。UX毀損 + 開発者の信頼低下。 +- **緩和策**: + - 実機でiOS Safariの実際の挙動を確認(5月最初の2週間のチェックリストに追加) + - もし二重表示があれば、SmartAppBanner側のJS実装を `if (isIOSSafari) return null` で抑制 + - 計測上は「Safariからのクリック」「それ以外からのクリック」を分離して計測 + +### R3. Quick-win SEO施策が Google アルゴリズム更新で吹き飛ぶリスク +- **影響**: SC quick-win で得た +700クリックが消える可能性。 +- **緩和策**: + - SEO一本足にせず、X / signup後誘導 / 招待URL の **複数チャネル** を並行で立ち上げる + - SEO施策は「メタタグ最適化」中心(ペナルティリスクの低い王道施策) + - AI検索流入を早期に取りに行く(GoogleとAIで分散) + +### R4. 3つの並行エージェント提案の衝突解決方針 +**衝突パターンと解決原則**: +1. **計測 vs 実装スピード**: analytics-analystが「先に計測整備」、product-plannerが「先にCTA改善」を主張するケース。 + → **計測優先**(5月は計測なしでは効果検証できない)。ただし計測が30分以内で済むなら同時並行OK。 +2. **SEO vs 製品改善**: growth-marketingがSEO記事追加、product-plannerがCTA改善を主張するケース。 + → **既存トラフィックの転換率改善(CTA)を優先**。流入が増えても変換しないと意味がない。 +3. **施策の優先順位**: 全エージェントの提案を本マスタープランの「優先度マトリクス」に強制マッピングし、Quick Win → 高効果 → 中効果の順で実行。 +4. **競合する技術選定**: GA4のイベント命名、CTAコンポーネント設計などで競合した場合は、**既存実装との一貫性**を最優先(CalculatorFormCTAの設計を踏襲)。 + +### R5. 7月夏の大会シーズン中の登録鈍化 +- **影響**: 中高生が大会・遠征で忙しく、アプリ登録に手が回らない可能性。 +- **緩和策**: + - 大会前(6月後半)に「**大会前に登録すれば成績がそのまま蓄積される**」訴求を強化 + - 大会後(7月後半〜8月)の「**新チーム始動 × 招待URL**」が本来のピーク。7月は仕込み期と割り切る。 + +### R6. 個人開発の工数オーバーリスク +- **影響**: 並行業務(front, back, mobile)と戦略実行で工数破綻。 +- **緩和策**: + - 5月の最初の2週間は「計測 + Quick Win」のみ。新機能開発を完全に止める。 + - 週次レビューで進捗が遅れていたら、優先度マトリクスの「下段」を即座に切り捨てる。 + - 月10時間を切ったら、ロードマップを翌月に1ヶ月ずつ後ろ倒しする勇気を持つ。 + +--- + +## 8. 次のアクション(このドキュメント完成直後) + +1. analytics-analyst / product-planner / growth-marketing の3エージェントの結果ファイルが上がってきたら、本マスタープランに **個別施策の詳細** をマージする。 +2. 5/1の作業開始前に、本マスタープランの「5月最初の2週間タスク表」を `docs/strategy/pdca/cycle-202605.md` に転記し、進捗管理に使う。 +3. 5/4のチェックポイントで /tools/k-bb のCTA不具合の有無を必ず確認。 + +--- + +## 関連ドキュメント +- 前回統合レポート: [`ios-app-download-growth-integrated-202604.md`](./ios-app-download-growth-integrated-202604.md) +- 全体戦略: [`ios-app-growth-strategy-202604.md`](./ios-app-growth-strategy-202604.md) +- ASO: [`marketing/aso-and-app-growth-plan.md`](./marketing/aso-and-app-growth-plan.md) +- 競合分析: [`research/ios-growth-strategy-202604.md`](./research/ios-growth-strategy-202604.md) +- プロダクト企画: [`product/ios-app-download-growth.md`](./product/ios-app-download-growth.md) +- 収益化戦略: [`monetization-strategy-ios-web-202604.md`](./monetization-strategy-ios-web-202604.md) +- SNS成長: [`mobile-app-sns-growth-202604.md`](./mobile-app-sns-growth-202604.md) diff --git a/docs/superpowers/plans/2026-04-02-app-store-review-prompt.md b/docs/superpowers/plans/2026-04-02-app-store-review-prompt.md new file mode 100644 index 0000000..8c5c580 --- /dev/null +++ b/docs/superpowers/plans/2026-04-02-app-store-review-prompt.md @@ -0,0 +1,268 @@ +# App Storeレビュー促進施策 実装計画 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 試合記録完了時にマイルストーン条件を満たしたユーザーへOS標準のApp Storeレビューダイアログを表示する + +**Architecture:** `expo-store-review` で OS 標準ダイアログを表示し、表示条件の状態管理は `expo-secure-store` でローカルに保持。ロジックは `useStoreReview` カスタムフックに集約し、ルートレイアウトで初期化、試合記録完了画面から条件判定を呼び出す。 + +**Tech Stack:** Expo SDK 55, React Native, expo-store-review, expo-secure-store, TypeScript + +--- + +## ファイル構成 + +| ファイル | 操作 | 責務 | +|---------|------|------| +| `mobile/hooks/useStoreReview.ts` | 新規作成 | レビュー表示の条件判定・状態管理・実行 | +| `mobile/app/_layout.tsx` | 修正 | `initInstallDate()` の呼び出し追加(1行) | +| `mobile/app/(game-record)/summary.tsx` | 修正 | `checkAndRequestReview()` の呼び出し追加(数行) | + +--- + +### Task 1: expo-store-review パッケージのインストール + +**Files:** +- Modify: `mobile/package.json` + +- [ ] **Step 1: パッケージをインストール** + +```bash +cd /Users/shimizuippei/projects/dev/buzzbase/mobile && yarn add expo-store-review +``` + +- [ ] **Step 2: インストール確認** + +```bash +cd /Users/shimizuippei/projects/dev/buzzbase/mobile && grep expo-store-review package.json +``` + +Expected: `"expo-store-review": "~X.X.X"` が表示される + +- [ ] **Step 3: コミット** + +```bash +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile add package.json yarn.lock +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile commit -m "Add: expo-store-reviewパッケージを追加" +``` + +--- + +### Task 2: useStoreReview フックの作成 + +**Files:** +- Create: `mobile/hooks/useStoreReview.ts` + +- [ ] **Step 1: フックファイルを作成** + +```typescript +import { useCallback } from "react"; +import * as SecureStore from "expo-secure-store"; +import * as StoreReview from "expo-store-review"; + +const KEYS = { + GAME_COUNT: "store_review_game_count", + INSTALL_DATE: "store_review_install_date", + LAST_SHOWN: "store_review_last_shown", + SHOWN_COUNT: "store_review_shown_count", + SHOWN_YEAR: "store_review_shown_year", +} as const; + +const MILESTONES = [5, 20, 50, 80, 100]; +const MIN_DAYS_SINCE_INSTALL = 7; +const MAX_SHOWS_PER_YEAR = 3; +const MIN_DAYS_BETWEEN_SHOWS = 90; + +function daysSince(dateString: string | null): number { + if (!dateString) return Infinity; + const then = new Date(dateString).getTime(); + const now = Date.now(); + return Math.floor((now - then) / (1000 * 60 * 60 * 24)); +} + +export const useStoreReview = () => { + const initInstallDate = useCallback(async () => { + const existing = await SecureStore.getItemAsync(KEYS.INSTALL_DATE); + if (existing) return; + await SecureStore.setItemAsync(KEYS.INSTALL_DATE, new Date().toISOString()); + }, []); + + const checkAndRequestReview = useCallback(async () => { + // 1. game_count をインクリメント + const currentCount = await SecureStore.getItemAsync(KEYS.GAME_COUNT); + const newCount = (parseInt(currentCount ?? "0", 10) || 0) + 1; + await SecureStore.setItemAsync(KEYS.GAME_COUNT, String(newCount)); + + // 2. マイルストーンに一致するか + if (!MILESTONES.includes(newCount)) return; + + // 3. インストールから7日以上経過しているか + const installDate = await SecureStore.getItemAsync(KEYS.INSTALL_DATE); + if (daysSince(installDate) < MIN_DAYS_SINCE_INSTALL) return; + + // 4. 年の表示回数をチェック(年が変わっていたらリセット) + const currentYear = new Date().getFullYear(); + const storedYear = await SecureStore.getItemAsync(KEYS.SHOWN_YEAR); + let shownCount = parseInt( + (await SecureStore.getItemAsync(KEYS.SHOWN_COUNT)) ?? "0", + 10, + ) || 0; + + if (storedYear !== String(currentYear)) { + shownCount = 0; + } + + if (shownCount >= MAX_SHOWS_PER_YEAR) return; + + // 5. 前回表示から90日以上経過しているか(初回はスキップ) + const lastShown = await SecureStore.getItemAsync(KEYS.LAST_SHOWN); + if (lastShown && daysSince(lastShown) < MIN_DAYS_BETWEEN_SHOWS) return; + + // 6. 端末が対応しているか + const isAvailable = await StoreReview.isAvailableAsync(); + if (!isAvailable) return; + + // 7. レビューダイアログを表示 + await StoreReview.requestReview(); + + // 8. 表示状態を更新 + await SecureStore.setItemAsync(KEYS.LAST_SHOWN, new Date().toISOString()); + await SecureStore.setItemAsync(KEYS.SHOWN_COUNT, String(shownCount + 1)); + await SecureStore.setItemAsync(KEYS.SHOWN_YEAR, String(currentYear)); + }, []); + + return { initInstallDate, checkAndRequestReview }; +}; +``` + +- [ ] **Step 2: TypeScriptの型チェック** + +```bash +cd /Users/shimizuippei/projects/dev/buzzbase/mobile && yarn typecheck +``` + +Expected: エラーなし + +- [ ] **Step 3: コミット** + +```bash +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile add hooks/useStoreReview.ts +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile commit -m "Add: useStoreReviewフックを作成" +``` + +--- + +### Task 3: ルートレイアウトに initInstallDate を追加 + +**Files:** +- Modify: `mobile/app/_layout.tsx:5,12` + +- [ ] **Step 1: import文を追加** + +`mobile/app/_layout.tsx` の5行目の後に追加: + +```typescript +import { useStoreReview } from "@hooks/useStoreReview"; +``` + +- [ ] **Step 2: RootLayoutInner 内でフックを呼び出し** + +`mobile/app/_layout.tsx` の `RootLayoutInner` 関数内、`usePushNotifications();` の直後に追加: + +```typescript + const { initInstallDate } = useStoreReview(); + initInstallDate(); +``` + +変更後の `RootLayoutInner` は以下のようになる: + +```typescript +function RootLayoutInner() { + usePushNotifications(); + const { initInstallDate } = useStoreReview(); + initInstallDate(); + + return ( + <> + <StatusBar style="light" /> + ... +``` + +- [ ] **Step 3: TypeScriptの型チェック** + +```bash +cd /Users/shimizuippei/projects/dev/buzzbase/mobile && yarn typecheck +``` + +Expected: エラーなし + +- [ ] **Step 4: コミット** + +```bash +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile add app/_layout.tsx +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile commit -m "Add: ルートレイアウトにインストール日記録を追加" +``` + +--- + +### Task 4: 試合記録完了画面に checkAndRequestReview を追加 + +**Files:** +- Modify: `mobile/app/(game-record)/summary.tsx:2,6,59-65` + +- [ ] **Step 1: import文を追加** + +`mobile/app/(game-record)/summary.tsx` の6行目の後に追加: + +```typescript +import { useStoreReview } from "@hooks/useStoreReview"; +``` + +- [ ] **Step 2: フックを呼び出し、handleComplete を修正** + +`SummaryScreen` コンポーネント内でフックを呼び出す。13行目(`const store = useGameRecordStore();`)の後に追加: + +```typescript + const { checkAndRequestReview } = useStoreReview(); +``` + +`handleComplete` を async に変更し、`checkAndRequestReview()` を呼び出す。ルーター遷移の前に呼ぶ: + +```typescript + const handleComplete = async () => { + resetFlow(); + queryClient.invalidateQueries({ queryKey: ["dashboard"] }); + queryClient.invalidateQueries({ queryKey: ["gameResults"] }); + queryClient.invalidateQueries({ queryKey: ["userGameResults"] }); + await checkAndRequestReview(); + router.replace("/(tabs)/(game-results)"); + }; +``` + +- [ ] **Step 3: TypeScriptの型チェック** + +```bash +cd /Users/shimizuippei/projects/dev/buzzbase/mobile && yarn typecheck +``` + +Expected: エラーなし + +- [ ] **Step 4: コミット** + +```bash +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile add app/\(game-record\)/summary.tsx +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile commit -m "Add: 試合記録完了時にApp Storeレビュー表示を追加" +``` + +--- + +## 手動テスト手順 + +実機(iOS)での確認: + +1. アプリをクリーンインストールして起動 → `store_review_install_date` が記録されることを確認 +2. 試合記録を完了 → `store_review_game_count` がインクリメントされることを確認 +3. 5回目の試合記録完了時(インストールから7日以上経過している場合) → レビューダイアログが表示されることを確認 +4. ダイアログ表示後、`store_review_last_shown`, `store_review_shown_count`, `store_review_shown_year` が更新されることを確認 + +※ 開発中は `StoreReview.isAvailableAsync()` が `false` を返す場合がある(シミュレータ等)。実機TestFlightビルドでの確認を推奨。 diff --git a/docs/superpowers/plans/2026-04-03-stats-page.md b/docs/superpowers/plans/2026-04-03-stats-page.md new file mode 100644 index 0000000..bab072c --- /dev/null +++ b/docs/superpowers/plans/2026-04-03-stats-page.md @@ -0,0 +1,2564 @@ +# 成績ページ実装計画 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** モバイルアプリのRecordタブを成績ページに差し替え、打球分布図・成績テーブル・試合統計を表示する + +**Architecture:** バックエンドに成績集計用の新APIエンドポイント群を追加し、モバイルアプリに新しい成績ページUIを構築する。既存の打球方向データ(9方向)を13方向に拡張するDBマイグレーションを含む。 + +**Tech Stack:** Rails API (Ruby), React Native (Expo), react-native-svg, @tanstack/react-query, Zustand + +**設計書:** `docs/superpowers/specs/2026-04-03-stats-page-design.md` + +--- + +## ファイル構成 + +### バックエンド(back/) + +``` +db/migrate/ + XXXXXX_remap_batting_position_ids.rb # 既存データのID再マッピング + +app/controllers/api/v2/stats_controller.rb # 成績集計API + +app/services/stats/ + hit_direction_aggregator.rb # 打球方向集計 + plate_appearance_breakdown_service.rb # 打席結果内訳 + batting_stats_table_service.rb # 打撃成績テーブル(年/月/日) + pitching_stats_table_service.rb # 投球成績テーブル(年/月/日) + game_summary_service.rb # 試合結果統計 + +config/routes.rb # ルーティング追加 + +spec/requests/api/v2/stats_spec.rb # APIテスト +spec/services/stats/ # サービステスト +``` + +### モバイル(mobile/) + +``` +constants/battingData.ts # 修正: 13方向に拡張 + +types/stats.ts # 新規: 成績ページ用型定義 + +services/statsService.ts # 新規: 成績API呼び出し + +hooks/useStats.ts # 新規: React Queryフック + +components/stats/ + SprayChart.tsx # 打球分布図(バブルチャート) + PlateAppearanceDonut.tsx # 打席結果内訳(ドーナツ) + StatsTable.tsx # 成績テーブル(打撃・投球共用) + PeriodToggle.tsx # 年/月/日切り替え + GameResultSummary.tsx # 試合結果統計セクション + WinLossCards.tsx # 勝敗カード + 勝率バー + MatchTypeBreakdown.tsx # 試合種別 + MonthlyGameChart.tsx # 月別試合数棒グラフ + OpponentRecord.tsx # 対戦相手別勝敗 + StatsFilters.tsx # フィルタUI(年度・種別・シーズン) + +app/(tabs)/stats.tsx # 新規: 成績ページ(recordを差し替え) +app/(tabs)/_layout.tsx # 修正: タブ差し替え +``` + +--- + +## Task 1: バックエンド — batting_position_id マイグレーション + +**Files:** +- Create: `back/db/migrate/XXXXXX_remap_batting_position_ids.rb` + +既存の打球方向ID(7=左, 8=中, 9=右)を新体系(8=左, 10=中, 12=右)に再マッピングし、新しい4方向のスペースを確保する。 + +**新しいID体系:** + +| 旧ID | 旧ラベル | 新ID | 新ラベル | +|------|---------|------|---------| +| 1 | 投 | 1 | 投 | +| 2 | 捕 | 2 | 捕 | +| 3 | 一 | 3 | 一 | +| 4 | 二 | 4 | 二 | +| 5 | 三 | 5 | 三 | +| 6 | 遊 | 6 | 遊 | +| 7 | 左 | 8 | 左 | +| 8 | 中 | 10 | 中 | +| 9 | 右 | 12 | 右 | +| — | — | 7 | 左線(三塁線) | +| — | — | 9 | 左中(左中間) | +| — | — | 11 | 右中(右中間) | +| — | — | 13 | 右線(一塁線) | + +- [ ] **Step 1: マイグレーションファイルを作成** + +```bash +docker compose exec back bundle exec rails generate migration RemapBattingPositionIds +``` + +- [ ] **Step 2: マイグレーション内容を記述** + +```ruby +# back/db/migrate/XXXXXX_remap_batting_position_ids.rb +class RemapBattingPositionIds < ActiveRecord::Migration[7.0] + def up + # 衝突を避けるため、大きい番号から順に更新 + # 9(右) → 12 + execute "UPDATE plate_appearances SET batting_position_id = 12 WHERE batting_position_id = 9" + # 8(中) → 10 + execute "UPDATE plate_appearances SET batting_position_id = 10 WHERE batting_position_id = 8" + # 7(左) → 8 + execute "UPDATE plate_appearances SET batting_position_id = 8 WHERE batting_position_id = 7" + end + + def down + execute "UPDATE plate_appearances SET batting_position_id = 7 WHERE batting_position_id = 8" + execute "UPDATE plate_appearances SET batting_position_id = 8 WHERE batting_position_id = 10" + execute "UPDATE plate_appearances SET batting_position_id = 9 WHERE batting_position_id = 12" + end +end +``` + +- [ ] **Step 3: マイグレーション実行** + +```bash +docker compose exec back bundle exec rails db:migrate +``` + +- [ ] **Step 4: データ確認** + +```bash +docker compose exec back bundle exec rails runner " + puts 'ID分布:' + PlateAppearance.group(:batting_position_id).count.sort.each { |id, c| puts \" ID #{id}: #{c}件\" } + puts '旧ID(7,8,9)の残存: ' + PlateAppearance.where(batting_position_id: [7, 8, 9]).count.to_s + '件(0であること)' +" +``` + +Expected: 旧ID 7,8,9 は0件。新ID 8,10,12 にデータが移動。 + +- [ ] **Step 5: コミット** + +```bash +git -C /Users/shimizuippei/projects/dev/buzzbase/back add -A +git -C /Users/shimizuippei/projects/dev/buzzbase/back commit -m "Fix: batting_position_idを新13方向体系にリマッピング" +``` + +--- + +## Task 2: バックエンド — 成績集計サービス群 + +**Files:** +- Create: `back/app/services/stats/hit_direction_aggregator.rb` +- Create: `back/app/services/stats/plate_appearance_breakdown_service.rb` +- Create: `back/app/services/stats/batting_stats_table_service.rb` +- Create: `back/app/services/stats/pitching_stats_table_service.rb` +- Create: `back/app/services/stats/game_summary_service.rb` + +- [ ] **Step 1: servicesディレクトリ作成** + +```bash +docker compose exec back mkdir -p app/services/stats +``` + +- [ ] **Step 2: 打球方向集計サービス** + +```ruby +# back/app/services/stats/hit_direction_aggregator.rb +module Stats + class HitDirectionAggregator + DIRECTIONS = { + 1 => "投", 2 => "捕", 3 => "一", 4 => "二", 5 => "三", 6 => "遊", + 7 => "左線", 8 => "左", 9 => "左中", 10 => "中", + 11 => "右中", 12 => "右", 13 => "右線" + }.freeze + + def initialize(user_id:, year: nil, match_type: nil, season_id: nil) + @user_id = user_id + @year = year + @match_type = match_type + @season_id = season_id + end + + def call + pa = PlateAppearance.joins(game_result: :match_result) + .where(user_id: @user_id) + .where.not(batting_position_id: [0, nil]) + + pa = pa.where("EXTRACT(YEAR FROM match_results.date_and_time) = ?", @year) if @year.present? + pa = pa.where(match_results: { match_type: @match_type }) if @match_type.present? && @match_type != "全て" + pa = pa.where(game_results: { season_id: @season_id }) if @season_id.present? + + counts = pa.group(:batting_position_id).count + + DIRECTIONS.map do |id, label| + { id: id, label: label, count: counts[id] || 0 } + end + end + end +end +``` + +- [ ] **Step 3: 打席結果内訳サービス** + +```ruby +# back/app/services/stats/plate_appearance_breakdown_service.rb +module Stats + class PlateAppearanceBreakdownService + CATEGORIES = { + "安打" => [7, 8, 9, 10], + "ゴロ" => [1], + "フライ" => [2, 3, 4], + "三振" => [13, 14], + "四死球" => [15, 16], + "その他" => [5, 6, 11, 12, 17, 18, 19] + }.freeze + + def initialize(user_id:, year: nil, match_type: nil, season_id: nil) + @user_id = user_id + @year = year + @match_type = match_type + @season_id = season_id + end + + def call + pa = PlateAppearance.joins(game_result: :match_result) + .where(user_id: @user_id) + .where.not(plate_result_id: [0, nil]) + + pa = pa.where("EXTRACT(YEAR FROM match_results.date_and_time) = ?", @year) if @year.present? + pa = pa.where(match_results: { match_type: @match_type }) if @match_type.present? && @match_type != "全て" + pa = pa.where(game_results: { season_id: @season_id }) if @season_id.present? + + result_counts = pa.group(:plate_result_id).count + total = result_counts.values.sum + + CATEGORIES.map do |category, result_ids| + count = result_ids.sum { |id| result_counts[id] || 0 } + { + category: category, + count: count, + percentage: total > 0 ? (count.to_f / total * 100).round(1) : 0.0 + } + end + end + end +end +``` + +- [ ] **Step 4: 打撃成績テーブルサービス** + +```ruby +# back/app/services/stats/batting_stats_table_service.rb +module Stats + class BattingStatsTableService + def initialize(user_id:, period: "yearly", year: nil) + @user_id = user_id + @period = period + @year = year + end + + def call + case @period + when "yearly" then yearly_stats + when "monthly" then monthly_stats + when "daily" then daily_stats + end + end + + private + + def yearly_stats + years = MatchResult.joins(game_result: :batting_average) + .where(game_results: { user_id: @user_id }) + .select("DISTINCT EXTRACT(YEAR FROM date_and_time) AS year") + .map { |r| r.year.to_i } + .sort + + rows = years.map { |y| build_batting_row(y.to_s, year: y) } + rows << build_career_total_row if rows.any? + rows + end + + def monthly_stats + return [] unless @year + + months = MatchResult.joins(game_result: :batting_average) + .where(game_results: { user_id: @user_id }) + .where("EXTRACT(YEAR FROM date_and_time) = ?", @year) + .select("DISTINCT EXTRACT(MONTH FROM date_and_time) AS month") + .map { |r| r.month.to_i } + .sort + + rows = months.map { |m| build_batting_row("#{m}月", year: @year, month: m) } + rows << build_year_total_row if rows.any? + rows + end + + def daily_stats + return [] unless @year + + game_results = GameResult.includes(:match_result, :batting_average) + .where(user_id: @user_id) + .joins(:match_result) + .where("EXTRACT(YEAR FROM match_results.date_and_time) = ?", @year) + .order("match_results.date_and_time ASC") + + rows = game_results.filter_map do |gr| + next unless gr.batting_average + ba = gr.batting_average + mr = gr.match_result + date_label = mr.date_and_time.strftime("%-m/%-d") + opponent = mr.respond_to?(:opponent_team_name) ? mr.opponent_team_name : "" + label = "#{date_label} vs #{opponent}" + build_row_from_batting_average(label, ba, 1) + end + rows << build_year_total_row if rows.any? + rows + end + + def build_batting_row(label, year: nil, month: nil) + scope = BattingAverage.joins(game_result: :match_result) + .where(game_results: { user_id: @user_id }) + scope = scope.where("EXTRACT(YEAR FROM match_results.date_and_time) = ?", year) if year + scope = scope.where("EXTRACT(MONTH FROM match_results.date_and_time) = ?", month) if month + + agg = scope.select( + "COUNT(*) as games", + "SUM(plate_appearances) as plate_appearances", + "SUM(at_bats) as at_bats", + "SUM(hit) as hit", + "SUM(two_base_hit) as two_base_hit", + "SUM(three_base_hit) as three_base_hit", + "SUM(home_run) as home_run", + "SUM(total_bases) as total_bases", + "SUM(runs_batted_in) as runs_batted_in", + "SUM(run) as run", + "SUM(strike_out) as strike_out", + "SUM(base_on_balls) as base_on_balls", + "SUM(hit_by_pitch) as hit_by_pitch", + "SUM(sacrifice_hit) as sacrifice_hit", + "SUM(sacrifice_fly) as sacrifice_fly", + "SUM(stealing_base) as stealing_base", + "SUM(caught_stealing) as caught_stealing", + "SUM(error) as error" + ).take + + format_batting_row(label, agg) + end + + def build_row_from_batting_average(label, ba, games) + format_batting_row(label, ba, games_override: games) + end + + def build_career_total_row + build_batting_row("通算") + end + + def build_year_total_row + build_batting_row("通算", year: @year) + end + + def format_batting_row(label, agg, games_override: nil) + return nil unless agg + + at_bats = agg.at_bats.to_i + hit = agg.hit.to_i + two_base_hit = agg.two_base_hit.to_i + three_base_hit = agg.three_base_hit.to_i + home_run = agg.home_run.to_i + total_bases = agg.total_bases.to_i + bb = agg.base_on_balls.to_i + hbp = agg.hit_by_pitch.to_i + sf = agg.sacrifice_fly.to_i + so = agg.strike_out.to_i + pa = agg.plate_appearances.to_i + + batting_avg = at_bats > 0 ? (hit.to_f / at_bats).round(3) : 0.0 + slg = at_bats > 0 ? (total_bases.to_f / at_bats).round(3) : 0.0 + obp_denom = at_bats + bb + hbp + sf + obp = obp_denom > 0 ? ((hit + bb + hbp).to_f / obp_denom).round(3) : 0.0 + ops = (obp + slg).round(3) + iso = (slg - batting_avg).round(3) + bb_k = so > 0 ? (bb.to_f / so).round(3) : 0.0 + # BABIP = (H - HR) / (AB - SO - HR + SF) + babip_denom = at_bats - so - home_run + sf + babip = babip_denom > 0 ? ((hit - home_run).to_f / babip_denom).round(3) : 0.0 + + { + label: label, + games: games_override || (agg.respond_to?(:games) ? agg.games.to_i : 0), + plate_appearances: pa, + at_bats: at_bats, + hit: hit, + two_base_hit: two_base_hit, + three_base_hit: three_base_hit, + home_run: home_run, + total_bases: total_bases, + runs_batted_in: agg.runs_batted_in.to_i, + run: agg.run.to_i, + strike_out: so, + base_on_balls: bb, + hit_by_pitch: hbp, + sacrifice_hit: agg.sacrifice_hit.to_i, + sacrifice_fly: sf, + stealing_base: agg.stealing_base.to_i, + caught_stealing: agg.caught_stealing.to_i, + error: agg.error.to_i, + batting_average: batting_avg, + slugging_percentage: slg, + ops: ops, + iso: iso, + bb_per_k: bb_k, + babip: babip + } + end + end +end +``` + +- [ ] **Step 5: 投球成績テーブルサービス** + +```ruby +# back/app/services/stats/pitching_stats_table_service.rb +module Stats + class PitchingStatsTableService + def initialize(user_id:, period: "yearly", year: nil) + @user_id = user_id + @period = period + @year = year + end + + def call + case @period + when "yearly" then yearly_stats + when "monthly" then monthly_stats + when "daily" then daily_stats + end + end + + private + + def yearly_stats + years = MatchResult.joins(game_result: :pitching_result) + .where(game_results: { user_id: @user_id }) + .select("DISTINCT EXTRACT(YEAR FROM date_and_time) AS year") + .map { |r| r.year.to_i } + .sort + + rows = years.map { |y| build_pitching_row(y.to_s, year: y) } + rows << build_career_total_row if rows.any? + rows + end + + def monthly_stats + return [] unless @year + + months = MatchResult.joins(game_result: :pitching_result) + .where(game_results: { user_id: @user_id }) + .where("EXTRACT(YEAR FROM date_and_time) = ?", @year) + .select("DISTINCT EXTRACT(MONTH FROM date_and_time) AS month") + .map { |r| r.month.to_i } + .sort + + rows = months.map { |m| build_pitching_row("#{m}月", year: @year, month: m) } + rows << build_year_total_row if rows.any? + rows + end + + def daily_stats + return [] unless @year + + game_results = GameResult.includes(:match_result, :pitching_result) + .where(user_id: @user_id) + .joins(:match_result) + .where("EXTRACT(YEAR FROM match_results.date_and_time) = ?", @year) + .order("match_results.date_and_time ASC") + + rows = game_results.filter_map do |gr| + next unless gr.pitching_result + pr = gr.pitching_result + mr = gr.match_result + date_label = mr.date_and_time.strftime("%-m/%-d") + opponent = mr.respond_to?(:opponent_team_name) ? mr.opponent_team_name : "" + label = "#{date_label} vs #{opponent}" + build_row_from_pitching_result(label, pr, 1) + end + rows << build_year_total_row if rows.any? + rows + end + + def build_pitching_row(label, year: nil, month: nil) + scope = PitchingResult.joins(game_result: :match_result) + .where(game_results: { user_id: @user_id }) + scope = scope.where("EXTRACT(YEAR FROM match_results.date_and_time) = ?", year) if year + scope = scope.where("EXTRACT(MONTH FROM match_results.date_and_time) = ?", month) if month + + agg = scope.select( + "COUNT(*) as appearances", + "SUM(win) as win", "SUM(loss) as loss", + "SUM(hold) as hold", "SUM(saves) as saves", + "SUM(innings_pitched) as innings_pitched", + "SUM(hits_allowed) as hits_allowed", + "SUM(home_runs_hit) as home_runs_hit", + "SUM(strikeouts) as strikeouts", + "SUM(base_on_balls) as base_on_balls", + "SUM(hit_by_pitch) as hit_by_pitch", + "SUM(run_allowed) as run_allowed", + "SUM(earned_run) as earned_run", + "SUM(CASE WHEN got_to_the_distance THEN 1 ELSE 0 END) as complete_games" + ).take + + format_pitching_row(label, agg) + end + + def build_row_from_pitching_result(label, pr, appearances) + format_pitching_row(label, pr, appearances_override: appearances) + end + + def build_career_total_row + build_pitching_row("通算") + end + + def build_year_total_row + build_pitching_row("通算", year: @year) + end + + def format_pitching_row(label, agg, appearances_override: nil) + return nil unless agg + + ip = agg.innings_pitched.to_f + er = agg.earned_run.to_i + ha = agg.hits_allowed.to_i + bb = agg.base_on_balls.to_i + so = agg.strikeouts.to_i + w = agg.win.to_i + l = agg.loss.to_i + + era = ip > 0 ? (er.to_f / ip * 9).round(2) : 0.0 + whip = ip > 0 ? ((ha + bb).to_f / ip).round(2) : 0.0 + k9 = ip > 0 ? (so.to_f / ip * 9).round(2) : 0.0 + bb9 = ip > 0 ? (bb.to_f / ip * 9).round(2) : 0.0 + k_bb = bb > 0 ? (so.to_f / bb).round(2) : 0.0 + win_pct = (w + l) > 0 ? (w.to_f / (w + l)).round(3) : 0.0 + + { + label: label, + appearances: appearances_override || (agg.respond_to?(:appearances) ? agg.appearances.to_i : 0), + win: w, loss: l, + hold: agg.hold.to_i, saves: agg.saves.to_i, + complete_games: agg.respond_to?(:complete_games) ? agg.complete_games.to_i : (agg.got_to_the_distance ? 1 : 0), + shutouts: 0, + innings_pitched: ip, + hits_allowed: ha, + home_runs_hit: agg.home_runs_hit.to_i, + strikeouts: so, + base_on_balls: bb, + hit_by_pitch: agg.hit_by_pitch.to_i, + earned_run: er, + era: era, whip: whip, k_per_nine: k9, bb_per_nine: bb9, + k_bb: k_bb, win_percentage: win_pct + } + end + end +end +``` + +- [ ] **Step 6: 試合結果統計サービス** + +```ruby +# back/app/services/stats/game_summary_service.rb +module Stats + class GameSummaryService + def initialize(user_id:, year: nil) + @user_id = user_id + @year = year + end + + def call + { + win_loss: win_loss_summary, + match_type_breakdown: match_type_breakdown, + monthly_games: monthly_games, + opponent_records: opponent_records + } + end + + private + + def base_scope + scope = MatchResult.joins(:game_result).where(game_results: { user_id: @user_id }) + scope = scope.where("EXTRACT(YEAR FROM date_and_time) = ?", @year) if @year.present? + scope + end + + def win_loss_summary + results = base_scope.pluck(:my_team_score, :opponent_team_score) + wins = results.count { |my, opp| my > opp } + losses = results.count { |my, opp| my < opp } + draws = results.count { |my, opp| my == opp } + total = results.size + { + wins: wins, losses: losses, draws: draws, total: total, + win_rate: (wins + losses) > 0 ? (wins.to_f / (wins + losses)).round(3) : 0.0 + } + end + + def match_type_breakdown + ["公式戦", "オープン戦"].map do |mt| + records = base_scope.where(match_type: mt).pluck(:my_team_score, :opponent_team_score) + wins = records.count { |my, opp| my > opp } + losses = records.count { |my, opp| my < opp } + draws = records.count { |my, opp| my == opp } + { + match_type: mt, total: records.size, + wins: wins, losses: losses, draws: draws, + win_rate: (wins + losses) > 0 ? (wins.to_f / (wins + losses)).round(3) : 0.0 + } + end + end + + def monthly_games + base_scope + .group("EXTRACT(MONTH FROM date_and_time)") + .count + .map { |month, count| { month: month.to_i, count: count } } + .sort_by { |r| r[:month] } + end + + def opponent_records + opponent_teams = base_scope.joins("INNER JOIN teams ON teams.id = match_results.opponent_team_id") + .select("teams.name as team_name, match_results.my_team_score, match_results.opponent_team_score") + + grouped = opponent_teams.group_by(&:team_name) + grouped.map do |name, records| + wins = records.count { |r| r.my_team_score > r.opponent_team_score } + losses = records.count { |r| r.my_team_score < r.opponent_team_score } + draws = records.count { |r| r.my_team_score == r.opponent_team_score } + { team_name: name, wins: wins, losses: losses, draws: draws, total: records.size } + end.sort_by { |r| -r[:total] } + end + end +end +``` + +- [ ] **Step 7: コミット** + +```bash +git -C /Users/shimizuippei/projects/dev/buzzbase/back add app/services/stats/ +git -C /Users/shimizuippei/projects/dev/buzzbase/back commit -m "Add: 成績集計サービス群を追加(打球方向・打席内訳・成績テーブル・試合統計)" +``` + +--- + +## Task 3: バックエンド — Stats APIコントローラー + ルーティング + +**Files:** +- Create: `back/app/controllers/api/v2/stats_controller.rb` +- Modify: `back/config/routes.rb` + +- [ ] **Step 1: コントローラー作成** + +```ruby +# back/app/controllers/api/v2/stats_controller.rb +module Api + module V2 + class StatsController < ApplicationController + before_action :authenticate_api_v1_user! + + def hit_directions + result = Stats::HitDirectionAggregator.new( + user_id: target_user_id, + year: params[:year], + match_type: params[:match_type], + season_id: params[:season_id] + ).call + + render json: { directions: result } + end + + def plate_appearance_breakdown + result = Stats::PlateAppearanceBreakdownService.new( + user_id: target_user_id, + year: params[:year], + match_type: params[:match_type], + season_id: params[:season_id] + ).call + + render json: { breakdown: result } + end + + def batting + result = Stats::BattingStatsTableService.new( + user_id: target_user_id, + period: params[:period] || "yearly", + year: params[:year] + ).call + + render json: { rows: result } + end + + def pitching + result = Stats::PitchingStatsTableService.new( + user_id: target_user_id, + period: params[:period] || "yearly", + year: params[:year] + ).call + + render json: { rows: result } + end + + def game_summary + result = Stats::GameSummaryService.new( + user_id: target_user_id, + year: params[:year] + ).call + + render json: result + end + + private + + def target_user_id + params[:user_id] || current_api_v1_user.id + end + end + end +end +``` + +- [ ] **Step 2: ルーティング追加** + +`back/config/routes.rb` の `namespace :v2` ブロック内に追加: + +```ruby +# namespace :v2 do ブロック内、既存のresource :dashboardの後に追加 +resource :stats, only: [], controller: 'stats' do + get :hit_directions, on: :member + get :plate_appearance_breakdown, on: :member + get :batting, on: :member + get :pitching, on: :member + get :game_summary, on: :member +end +``` + +- [ ] **Step 3: ルーティング確認** + +```bash +docker compose exec back bundle exec rails routes | grep stats +``` + +Expected: +``` +hit_directions_stats GET /api/v2/stats/hit_directions +plate_appearance_breakdown_stats GET /api/v2/stats/plate_appearance_breakdown +batting_stats GET /api/v2/stats/batting +pitching_stats GET /api/v2/stats/pitching +game_summary_stats GET /api/v2/stats/game_summary +``` + +- [ ] **Step 4: APIの動作確認(Railsコンソール)** + +```bash +docker compose exec back bundle exec rails runner " + user = User.first + puts '=== Hit Directions ===' + puts Stats::HitDirectionAggregator.new(user_id: user.id).call.inspect + puts '=== PA Breakdown ===' + puts Stats::PlateAppearanceBreakdownService.new(user_id: user.id).call.inspect + puts '=== Batting Yearly ===' + puts Stats::BattingStatsTableService.new(user_id: user.id, period: 'yearly').call.length.to_s + ' rows' + puts '=== Game Summary ===' + puts Stats::GameSummaryService.new(user_id: user.id).call.inspect +" +``` + +- [ ] **Step 5: コミット** + +```bash +git -C /Users/shimizuippei/projects/dev/buzzbase/back add app/controllers/api/v2/stats_controller.rb config/routes.rb +git -C /Users/shimizuippei/projects/dev/buzzbase/back commit -m "Add: 成績集計APIエンドポイントを追加(v2/stats)" +``` + +--- + +## Task 4: バックエンド — APIリクエストスペック + +**Files:** +- Create: `back/spec/requests/api/v2/stats_spec.rb` + +- [ ] **Step 1: テストファイル作成** + +```ruby +# back/spec/requests/api/v2/stats_spec.rb +require 'rails_helper' + +RSpec.describe "Api::V2::Stats", type: :request do + let(:user) { create(:user) } + let(:auth_headers) { user.create_new_auth_token } + + before do + team = create(:team, name: "テストチーム") + opponent = create(:team, name: "相手チーム") + + gr = create(:game_result, user: user) + create(:match_result, + game_result: gr, user: user, + my_team_id: team.id, opponent_team_id: opponent.id, + my_team_score: 5, opponent_team_score: 3, + match_type: "公式戦", date_and_time: Time.zone.local(2025, 6, 15)) + create(:batting_average, game_result: gr, user: user, hit: 2, at_bats: 4, plate_appearances: 5) + create(:plate_appearance, game_result: gr, user: user, batting_position_id: 8, plate_result_id: 7, batter_box_number: 1) + create(:plate_appearance, game_result: gr, user: user, batting_position_id: 10, plate_result_id: 1, batter_box_number: 2) + end + + describe "GET /api/v2/stats/hit_directions" do + it "returns direction counts" do + get "/api/v2/stats/hit_directions", headers: auth_headers + expect(response).to have_http_status(:ok) + json = JSON.parse(response.body) + expect(json["directions"]).to be_an(Array) + expect(json["directions"].length).to eq(13) + end + end + + describe "GET /api/v2/stats/plate_appearance_breakdown" do + it "returns category breakdown" do + get "/api/v2/stats/plate_appearance_breakdown", headers: auth_headers + expect(response).to have_http_status(:ok) + json = JSON.parse(response.body) + expect(json["breakdown"]).to be_an(Array) + expect(json["breakdown"].map { |b| b["category"] }).to include("安打", "ゴロ") + end + end + + describe "GET /api/v2/stats/batting" do + it "returns yearly batting stats" do + get "/api/v2/stats/batting", params: { period: "yearly" }, headers: auth_headers + expect(response).to have_http_status(:ok) + json = JSON.parse(response.body) + expect(json["rows"]).to be_an(Array) + expect(json["rows"].last["label"]).to eq("通算") + end + end + + describe "GET /api/v2/stats/pitching" do + it "returns yearly pitching stats" do + get "/api/v2/stats/pitching", params: { period: "yearly" }, headers: auth_headers + expect(response).to have_http_status(:ok) + json = JSON.parse(response.body) + expect(json["rows"]).to be_an(Array) + end + end + + describe "GET /api/v2/stats/game_summary" do + it "returns game summary" do + get "/api/v2/stats/game_summary", headers: auth_headers + expect(response).to have_http_status(:ok) + json = JSON.parse(response.body) + expect(json).to have_key("win_loss") + expect(json).to have_key("match_type_breakdown") + expect(json).to have_key("monthly_games") + expect(json).to have_key("opponent_records") + end + end +end +``` + +- [ ] **Step 2: テスト実行** + +```bash +docker compose exec back bundle exec rspec spec/requests/api/v2/stats_spec.rb +``` + +Expected: 5 examples, 0 failures(factoryが不足している場合は調整が必要) + +- [ ] **Step 3: コミット** + +```bash +git -C /Users/shimizuippei/projects/dev/buzzbase/back add spec/requests/api/v2/ +git -C /Users/shimizuippei/projects/dev/buzzbase/back commit -m "Test: 成績集計APIのリクエストスペックを追加" +``` + +--- + +## Task 5: モバイル — 打球方向定数を13方向に拡張 + +**Files:** +- Modify: `mobile/constants/battingData.ts` + +- [ ] **Step 1: battingResultsPositionsを更新** + +`mobile/constants/battingData.ts` の `battingResultsPositions` を以下に置き換え: + +```typescript +export const battingResultsPositions = [ + { id: 0, label: "-" }, + { id: 1, label: "投" }, + { id: 2, label: "捕" }, + { id: 3, label: "一" }, + { id: 4, label: "二" }, + { id: 5, label: "三" }, + { id: 6, label: "遊" }, + { id: 7, label: "左線" }, + { id: 8, label: "左" }, + { id: 9, label: "左中" }, + { id: 10, label: "中" }, + { id: 11, label: "右中" }, + { id: 12, label: "右" }, + { id: 13, label: "右線" }, +]; +``` + +- [ ] **Step 2: 型チェック** + +```bash +cd /Users/shimizuippei/projects/dev/buzzbase/mobile && yarn typecheck +``` + +Expected: エラーなし(IDは数値なので型の影響なし) + +- [ ] **Step 3: コミット** + +```bash +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile add constants/battingData.ts +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile commit -m "Update: 打球方向を9方向から13方向に拡張" +``` + +--- + +## Task 6: モバイル — 成績ページ用の型定義 + サービス + フック + +**Files:** +- Create: `mobile/types/stats.ts` +- Create: `mobile/services/statsService.ts` +- Create: `mobile/hooks/useStats.ts` + +- [ ] **Step 1: 型定義** + +```typescript +// mobile/types/stats.ts + +export interface HitDirection { + id: number; + label: string; + count: number; +} + +export interface PlateAppearanceCategory { + category: string; + count: number; + percentage: number; +} + +export interface BattingStatsRow { + label: string; + games: number; + plate_appearances: number; + at_bats: number; + hit: number; + two_base_hit: number; + three_base_hit: number; + home_run: number; + total_bases: number; + runs_batted_in: number; + run: number; + strike_out: number; + base_on_balls: number; + hit_by_pitch: number; + sacrifice_hit: number; + sacrifice_fly: number; + stealing_base: number; + caught_stealing: number; + error: number; + batting_average: number; + slugging_percentage: number; + ops: number; + iso: number; + bb_per_k: number; + babip: number; +} + +export interface PitchingStatsRow { + label: string; + appearances: number; + win: number; + loss: number; + hold: number; + saves: number; + complete_games: number; + shutouts: number; + innings_pitched: number; + hits_allowed: number; + home_runs_hit: number; + strikeouts: number; + base_on_balls: number; + hit_by_pitch: number; + earned_run: number; + era: number; + whip: number; + k_per_nine: number; + bb_per_nine: number; + k_bb: number; + win_percentage: number; +} + +export interface WinLossSummary { + wins: number; + losses: number; + draws: number; + total: number; + win_rate: number; +} + +export interface MatchTypeRecord { + match_type: string; + total: number; + wins: number; + losses: number; + draws: number; + win_rate: number; +} + +export interface MonthlyGame { + month: number; + count: number; +} + +export interface OpponentRecord { + team_name: string; + wins: number; + losses: number; + draws: number; + total: number; +} + +export interface GameSummary { + win_loss: WinLossSummary; + match_type_breakdown: MatchTypeRecord[]; + monthly_games: MonthlyGame[]; + opponent_records: OpponentRecord[]; +} + +export type StatsPeriod = "yearly" | "monthly" | "daily"; +``` + +- [ ] **Step 2: APIサービス** + +```typescript +// mobile/services/statsService.ts +import axiosInstance from "@utils/axiosInstance"; +import { API_BASE_URL } from "@constants/api"; +import type { + HitDirection, + PlateAppearanceCategory, + BattingStatsRow, + PitchingStatsRow, + GameSummary, + StatsPeriod, +} from "../types/stats"; +import type { StatsFilters } from "../types/profile"; + +const STATS_URL = `${API_BASE_URL}/api/v2/stats`; + +export const getHitDirections = async ( + filters: StatsFilters, +): Promise<HitDirection[]> => { + const params = new URLSearchParams(); + if (filters.year) params.append("year", filters.year); + if (filters.matchType) params.append("match_type", filters.matchType); + if (filters.seasonId) params.append("season_id", filters.seasonId); + const res = await axiosInstance.get(`${STATS_URL}/hit_directions?${params}`); + return res.data.directions; +}; + +export const getPlateAppearanceBreakdown = async ( + filters: StatsFilters, +): Promise<PlateAppearanceCategory[]> => { + const params = new URLSearchParams(); + if (filters.year) params.append("year", filters.year); + if (filters.matchType) params.append("match_type", filters.matchType); + if (filters.seasonId) params.append("season_id", filters.seasonId); + const res = await axiosInstance.get( + `${STATS_URL}/plate_appearance_breakdown?${params}`, + ); + return res.data.breakdown; +}; + +export const getBattingStatsTable = async ( + period: StatsPeriod, + year?: string, +): Promise<BattingStatsRow[]> => { + const params = new URLSearchParams(); + params.append("period", period); + if (year) params.append("year", year); + const res = await axiosInstance.get(`${STATS_URL}/batting?${params}`); + return res.data.rows; +}; + +export const getPitchingStatsTable = async ( + period: StatsPeriod, + year?: string, +): Promise<PitchingStatsRow[]> => { + const params = new URLSearchParams(); + params.append("period", period); + if (year) params.append("year", year); + const res = await axiosInstance.get(`${STATS_URL}/pitching?${params}`); + return res.data.rows; +}; + +export const getGameSummary = async (year?: string): Promise<GameSummary> => { + const params = year ? `?year=${year}` : ""; + const res = await axiosInstance.get(`${STATS_URL}/game_summary${params}`); + return res.data; +}; +``` + +- [ ] **Step 3: React Queryフック** + +```typescript +// mobile/hooks/useStats.ts +import { useQuery } from "@tanstack/react-query"; +import { + getHitDirections, + getPlateAppearanceBreakdown, + getBattingStatsTable, + getPitchingStatsTable, + getGameSummary, +} from "../services/statsService"; +import type { StatsFilters } from "../types/profile"; +import type { StatsPeriod } from "../types/stats"; + +export const useHitDirections = (filters: StatsFilters) => + useQuery({ + queryKey: ["hitDirections", filters], + queryFn: () => getHitDirections(filters), + }); + +export const usePlateAppearanceBreakdown = (filters: StatsFilters) => + useQuery({ + queryKey: ["paBreakdown", filters], + queryFn: () => getPlateAppearanceBreakdown(filters), + }); + +export const useBattingStatsTable = (period: StatsPeriod, year?: string) => + useQuery({ + queryKey: ["battingTable", period, year], + queryFn: () => getBattingStatsTable(period, year), + }); + +export const usePitchingStatsTable = (period: StatsPeriod, year?: string) => + useQuery({ + queryKey: ["pitchingTable", period, year], + queryFn: () => getPitchingStatsTable(period, year), + }); + +export const useGameSummary = (year?: string) => + useQuery({ + queryKey: ["gameSummary", year], + queryFn: () => getGameSummary(year), + }); +``` + +- [ ] **Step 4: 型チェック** + +```bash +cd /Users/shimizuippei/projects/dev/buzzbase/mobile && yarn typecheck +``` + +- [ ] **Step 5: コミット** + +```bash +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile add types/stats.ts services/statsService.ts hooks/useStats.ts +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile commit -m "Add: 成績ページ用の型定義・サービス・フックを追加" +``` + +--- + +## Task 7: モバイル — SprayChart コンポーネント + +**Files:** +- Create: `mobile/components/stats/SprayChart.tsx` + +- [ ] **Step 1: コンポーネント作成** + +```typescript +// mobile/components/stats/SprayChart.tsx +import React from "react"; +import { View, Text, StyleSheet } from "react-native"; +import Svg, { + Path, + Polygon, + Line, + Circle, + Text as SvgText, + Defs, + LinearGradient, + Stop, +} from "react-native-svg"; +import type { HitDirection } from "../../types/stats"; + +interface SprayChartProps { + directions: HitDirection[]; +} + +const WIDTH = 340; +const HEIGHT = 260; + +// 各方向のフィールド上の座標 (x, y) +const DIRECTION_POSITIONS: Record<number, { x: number; y: number }> = { + 1: { x: 170, y: 210 }, // 投 + 2: { x: 170, y: 245 }, // 捕 + 3: { x: 230, y: 190 }, // 一 + 4: { x: 200, y: 175 }, // 二 + 5: { x: 110, y: 175 }, // 三 + 6: { x: 140, y: 190 }, // 遊 + 7: { x: 48, y: 140 }, // 左線 + 8: { x: 65, y: 115 }, // 左 + 9: { x: 100, y: 85 }, // 左中 + 10: { x: 170, y: 65 }, // 中 + 11: { x: 240, y: 85 }, // 右中 + 12: { x: 275, y: 115 }, // 右 + 13: { x: 292, y: 140 }, // 右線 +}; + +const getBubbleRadius = (count: number, maxCount: number): number => { + if (count === 0 || maxCount === 0) return 0; + const minR = 8; + const maxR = 22; + return minR + (count / maxCount) * (maxR - minR); +}; + +const getBubbleColor = (count: number, maxCount: number): string => { + if (maxCount === 0) return "#6b7280"; + const ratio = count / maxCount; + if (ratio >= 0.7) return "#ef4444"; + if (ratio >= 0.4) return "#f59e0b"; + return "#3b82f6"; +}; + +const getBubbleOpacity = (count: number, maxCount: number): number => { + if (maxCount === 0) return 0; + return 0.4 + (count / maxCount) * 0.5; +}; + +export const SprayChart = ({ directions }: SprayChartProps) => { + const maxCount = Math.max(...directions.map((d) => d.count), 1); + + return ( + <View style={styles.container}> + <Text style={styles.title}>打球分布図</Text> + <Svg width={WIDTH} height={HEIGHT} viewBox={`0 0 ${WIDTH} ${HEIGHT}`}> + <Defs> + <LinearGradient id="fieldGrad" x1="0%" y1="0%" x2="0%" y2="100%"> + <Stop offset="0%" stopColor="#1a3a1a" /> + <Stop offset="100%" stopColor="#0d1f0d" /> + </LinearGradient> + </Defs> + + {/* Outfield arc */} + <Path + d={`M 15,${HEIGHT - 10} Q ${WIDTH / 2},10 ${WIDTH - 15},${HEIGHT - 10}`} + fill="url(#fieldGrad)" + stroke="#2a5a2a" + strokeWidth={1.5} + /> + + {/* Infield diamond */} + <Polygon + points={`${WIDTH / 2},${HEIGHT - 30} ${WIDTH / 2 + 45},${HEIGHT - 70} ${WIDTH / 2},${HEIGHT - 110} ${WIDTH / 2 - 45},${HEIGHT - 70}`} + fill="none" + stroke="#4a3a2a" + strokeWidth={1} + opacity={0.4} + /> + + {/* Foul lines */} + <Line + x1={WIDTH / 2} y1={HEIGHT} + x2={15} y2={70} + stroke="#444" strokeWidth={0.5} strokeDasharray="3,3" + /> + <Line + x1={WIDTH / 2} y1={HEIGHT} + x2={WIDTH - 15} y2={70} + stroke="#444" strokeWidth={0.5} strokeDasharray="3,3" + /> + + {/* Home plate */} + <Polygon + points={`${WIDTH / 2},${HEIGHT - 12} ${WIDTH / 2 - 4},${HEIGHT - 6} ${WIDTH / 2},${HEIGHT} ${WIDTH / 2 + 4},${HEIGHT - 6}`} + fill="white" + opacity={0.6} + /> + + {/* Bubbles */} + {directions.map((dir) => { + const pos = DIRECTION_POSITIONS[dir.id]; + if (!pos || dir.count === 0) return null; + const r = getBubbleRadius(dir.count, maxCount); + const color = getBubbleColor(dir.count, maxCount); + const opacity = getBubbleOpacity(dir.count, maxCount); + return ( + <React.Fragment key={dir.id}> + <Circle cx={pos.x} cy={pos.y} r={r} fill={color} opacity={opacity} /> + <SvgText + x={pos.x} y={pos.y + 4} + textAnchor="middle" + fill="white" + fontSize={r > 14 ? 11 : 9} + fontWeight="700" + > + {dir.count} + </SvgText> + </React.Fragment> + ); + })} + + {/* Labels for zero-count directions */} + {directions.map((dir) => { + const pos = DIRECTION_POSITIONS[dir.id]; + if (!pos || dir.count > 0) return null; + return ( + <SvgText + key={`label-${dir.id}`} + x={pos.x} y={pos.y + 3} + textAnchor="middle" + fill="#555" + fontSize={8} + > + {dir.label} + </SvgText> + ); + })} + </Svg> + </View> + ); +}; + +const styles = StyleSheet.create({ + container: { + backgroundColor: "#0d1f0d", + borderRadius: 12, + padding: 12, + marginBottom: 12, + }, + title: { + color: "#aaa", + fontSize: 12, + fontWeight: "600", + marginBottom: 8, + }, +}); +``` + +- [ ] **Step 2: 型チェック** + +```bash +cd /Users/shimizuippei/projects/dev/buzzbase/mobile && yarn typecheck +``` + +- [ ] **Step 3: コミット** + +```bash +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile add components/stats/SprayChart.tsx +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile commit -m "Add: 打球分布図(SprayChart)コンポーネントを追加" +``` + +--- + +## Task 8: モバイル — PlateAppearanceDonut コンポーネント + +**Files:** +- Create: `mobile/components/stats/PlateAppearanceDonut.tsx` + +- [ ] **Step 1: コンポーネント作成** + +```typescript +// mobile/components/stats/PlateAppearanceDonut.tsx +import React from "react"; +import { View, Text, StyleSheet } from "react-native"; +import Svg, { Circle, Text as SvgText } from "react-native-svg"; +import type { PlateAppearanceCategory } from "../../types/stats"; + +interface PlateAppearanceDonutProps { + breakdown: PlateAppearanceCategory[]; + totalPlateAppearances: number; +} + +const COLORS: Record<string, string> = { + "安打": "#ef4444", + "ゴロ": "#6b7280", + "フライ": "#3b82f6", + "三振": "#f59e0b", + "四死球": "#10b981", + "その他": "#8b5cf6", +}; + +const SIZE = 120; +const STROKE_WIDTH = 18; +const RADIUS = (SIZE - STROKE_WIDTH) / 2; +const CIRCUMFERENCE = 2 * Math.PI * RADIUS; + +export const PlateAppearanceDonut = ({ + breakdown, + totalPlateAppearances, +}: PlateAppearanceDonutProps) => { + let accumulatedOffset = 0; + + const segments = breakdown + .filter((cat) => cat.count > 0) + .map((cat) => { + const dashLength = (cat.percentage / 100) * CIRCUMFERENCE; + const dashGap = CIRCUMFERENCE - dashLength; + const offset = -accumulatedOffset + CIRCUMFERENCE * 0.25; // start from top + accumulatedOffset += dashLength; + return { + ...cat, + dashArray: `${dashLength} ${dashGap}`, + dashOffset: offset, + color: COLORS[cat.category] || "#6b7280", + }; + }); + + return ( + <View style={styles.container}> + <Text style={styles.title}>打席結果の内訳</Text> + <View style={styles.row}> + <Svg width={SIZE} height={SIZE} viewBox={`0 0 ${SIZE} ${SIZE}`}> + <Circle + cx={SIZE / 2} cy={SIZE / 2} r={RADIUS} + fill="none" stroke="#222" strokeWidth={STROKE_WIDTH} + /> + {segments.map((seg) => ( + <Circle + key={seg.category} + cx={SIZE / 2} cy={SIZE / 2} r={RADIUS} + fill="none" + stroke={seg.color} + strokeWidth={STROKE_WIDTH} + strokeDasharray={seg.dashArray} + strokeDashoffset={seg.dashOffset} + /> + ))} + <SvgText + x={SIZE / 2} y={SIZE / 2 - 6} + textAnchor="middle" fill="#fff" fontSize={16} fontWeight="700" + > + {totalPlateAppearances} + </SvgText> + <SvgText + x={SIZE / 2} y={SIZE / 2 + 10} + textAnchor="middle" fill="#888" fontSize={9} + > + 打席 + </SvgText> + </Svg> + + <View style={styles.legend}> + {breakdown.map((cat) => ( + <View key={cat.category} style={styles.legendItem}> + <View + style={[ + styles.legendDot, + { backgroundColor: COLORS[cat.category] || "#6b7280" }, + ]} + /> + <Text style={styles.legendLabel}>{cat.category}</Text> + <Text style={styles.legendValue}>{cat.percentage}%</Text> + </View> + ))} + </View> + </View> + </View> + ); +}; + +const styles = StyleSheet.create({ + container: { + backgroundColor: "#111", + borderRadius: 12, + padding: 12, + marginBottom: 12, + }, + title: { + color: "#aaa", + fontSize: 12, + fontWeight: "600", + marginBottom: 10, + }, + row: { + flexDirection: "row", + alignItems: "center", + gap: 16, + }, + legend: { + flex: 1, + gap: 4, + }, + legendItem: { + flexDirection: "row", + alignItems: "center", + gap: 6, + }, + legendDot: { + width: 8, + height: 8, + borderRadius: 4, + }, + legendLabel: { + color: "#ccc", + fontSize: 11, + flex: 1, + }, + legendValue: { + color: "#fff", + fontSize: 11, + fontWeight: "700", + }, +}); +``` + +- [ ] **Step 2: 型チェック + コミット** + +```bash +cd /Users/shimizuippei/projects/dev/buzzbase/mobile && yarn typecheck +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile add components/stats/PlateAppearanceDonut.tsx +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile commit -m "Add: 打席結果内訳(PlateAppearanceDonut)コンポーネントを追加" +``` + +--- + +## Task 9: モバイル — PeriodToggle + StatsTable コンポーネント + +**Files:** +- Create: `mobile/components/stats/PeriodToggle.tsx` +- Create: `mobile/components/stats/StatsTable.tsx` + +- [ ] **Step 1: PeriodToggle(年/月/日切り替え)** + +```typescript +// mobile/components/stats/PeriodToggle.tsx +import React from "react"; +import { View, Text, TouchableOpacity, StyleSheet } from "react-native"; +import type { StatsPeriod } from "../../types/stats"; + +interface PeriodToggleProps { + value: StatsPeriod; + onChange: (period: StatsPeriod) => void; +} + +const OPTIONS: { value: StatsPeriod; label: string }[] = [ + { value: "yearly", label: "年" }, + { value: "monthly", label: "月" }, + { value: "daily", label: "日" }, +]; + +export const PeriodToggle = ({ value, onChange }: PeriodToggleProps) => ( + <View style={styles.container}> + {OPTIONS.map((opt) => ( + <TouchableOpacity + key={opt.value} + style={[styles.option, value === opt.value && styles.optionActive]} + onPress={() => onChange(opt.value)} + > + <Text + style={[styles.label, value === opt.value && styles.labelActive]} + > + {opt.label} + </Text> + </TouchableOpacity> + ))} + </View> +); + +const styles = StyleSheet.create({ + container: { + flexDirection: "row", + backgroundColor: "#111", + borderRadius: 8, + padding: 2, + gap: 2, + }, + option: { + paddingVertical: 4, + paddingHorizontal: 12, + borderRadius: 6, + }, + optionActive: { + backgroundColor: "#f59e0b", + }, + label: { + fontSize: 11, + color: "#666", + fontWeight: "600", + }, + labelActive: { + color: "#000", + }, +}); +``` + +- [ ] **Step 2: StatsTable(共用成績テーブル)** + +```typescript +// mobile/components/stats/StatsTable.tsx +import React from "react"; +import { View, Text, ScrollView, StyleSheet } from "react-native"; +import type { BattingStatsRow, PitchingStatsRow } from "../../types/stats"; + +interface Column<T> { + key: keyof T; + label: string; + width: number; + format?: (value: number) => string; + highlight?: boolean; +} + +interface StatsTableProps<T> { + rows: T[]; + columns: Column<T>[]; + labelKey: keyof T; +} + +const fmt3 = (v: number) => v.toFixed(3).replace(/^0/, ""); +const fmt2 = (v: number) => v.toFixed(2); +const fmtInt = (v: number) => String(v); + +export const BATTING_COLUMNS: Column<BattingStatsRow>[] = [ + { key: "batting_average", label: "打率", width: 50, format: fmt3, highlight: true }, + { key: "games", label: "試合", width: 40, format: fmtInt }, + { key: "plate_appearances", label: "打席", width: 40, format: fmtInt }, + { key: "at_bats", label: "打数", width: 40, format: fmtInt }, + { key: "hit", label: "安打", width: 40, format: fmtInt }, + { key: "two_base_hit", label: "二塁打", width: 44, format: fmtInt }, + { key: "three_base_hit", label: "三塁打", width: 44, format: fmtInt }, + { key: "home_run", label: "本塁打", width: 44, format: fmtInt }, + { key: "total_bases", label: "塁打", width: 40, format: fmtInt }, + { key: "runs_batted_in", label: "打点", width: 40, format: fmtInt }, + { key: "run", label: "得点", width: 40, format: fmtInt }, + { key: "strike_out", label: "三振", width: 40, format: fmtInt }, + { key: "base_on_balls", label: "四球", width: 40, format: fmtInt }, + { key: "hit_by_pitch", label: "死球", width: 40, format: fmtInt }, + { key: "sacrifice_hit", label: "犠打", width: 40, format: fmtInt }, + { key: "sacrifice_fly", label: "犠飛", width: 40, format: fmtInt }, + { key: "stealing_base", label: "盗塁", width: 40, format: fmtInt }, + { key: "caught_stealing", label: "盗塁死", width: 44, format: fmtInt }, + { key: "error", label: "併殺打", width: 44, format: fmtInt }, + { key: "slugging_percentage", label: "長打率", width: 50, format: fmt3 }, + { key: "ops", label: "OPS", width: 50, format: fmt3 }, + { key: "iso", label: "ISO", width: 50, format: fmt3 }, + { key: "bb_per_k", label: "BB/K", width: 50, format: fmt3 }, + { key: "babip", label: "BABIP", width: 50, format: fmt3 }, +]; + +export const PITCHING_COLUMNS: Column<PitchingStatsRow>[] = [ + { key: "era", label: "防御率", width: 50, format: fmt2, highlight: true }, + { key: "appearances", label: "登板", width: 40, format: fmtInt }, + { key: "win", label: "勝利", width: 40, format: fmtInt }, + { key: "loss", label: "敗戦", width: 40, format: fmtInt }, + { key: "hold", label: "ホールド", width: 50, format: fmtInt }, + { key: "saves", label: "セーブ", width: 44, format: fmtInt }, + { key: "complete_games", label: "完投", width: 40, format: fmtInt }, + { key: "shutouts", label: "完封", width: 40, format: fmtInt }, + { key: "innings_pitched", label: "投球回", width: 48, format: fmt2 }, + { key: "hits_allowed", label: "被安打", width: 44, format: fmtInt }, + { key: "home_runs_hit", label: "被本塁打", width: 52, format: fmtInt }, + { key: "strikeouts", label: "三振", width: 40, format: fmtInt }, + { key: "base_on_balls", label: "四球", width: 40, format: fmtInt }, + { key: "hit_by_pitch", label: "死球", width: 40, format: fmtInt }, + { key: "earned_run", label: "自責点", width: 44, format: fmtInt }, + { key: "whip", label: "WHIP", width: 50, format: fmt2 }, + { key: "k_per_nine", label: "K/9", width: 46, format: fmt2 }, + { key: "bb_per_nine", label: "BB/9", width: 46, format: fmt2 }, + { key: "k_bb", label: "K/BB", width: 46, format: fmt2 }, +]; + +export function StatsTable<T extends { label: string }>({ + rows, + columns, + labelKey, +}: StatsTableProps<T>) { + const isCareerRow = (row: T) => row.label === "通算"; + + return ( + <View style={styles.tableContainer}> + <ScrollView horizontal showsHorizontalScrollIndicator={false}> + <View> + {/* Header */} + <View style={styles.headerRow}> + <View style={styles.stickyCell}> + <Text style={styles.headerLabelText}> + {String(labelKey) === "label" ? "" : String(labelKey)} + </Text> + </View> + {columns.map((col) => ( + <View key={String(col.key)} style={[styles.cell, { width: col.width }]}> + <Text style={styles.headerText}>{col.label}</Text> + </View> + ))} + </View> + + {/* Data rows */} + {rows.map((row, i) => { + const career = isCareerRow(row); + return ( + <View + key={i} + style={[ + styles.dataRow, + career && styles.careerRow, + !career && i < rows.length - 1 && styles.rowBorder, + ]} + > + <View style={[styles.stickyCell, career && styles.careerStickyCell]}> + <Text style={[styles.labelText, career && styles.careerLabelText]}> + {row[labelKey] as string} + </Text> + </View> + {columns.map((col) => { + const val = row[col.key] as number; + const formatted = col.format ? col.format(val) : String(val); + return ( + <View key={String(col.key)} style={[styles.cell, { width: col.width }]}> + <Text + style={[ + styles.cellText, + col.highlight && styles.highlightText, + career && styles.careerCellText, + ]} + > + {formatted} + </Text> + </View> + ); + })} + </View> + ); + })} + </View> + </ScrollView> + <Text style={styles.scrollHint}>← スクロール →</Text> + </View> + ); +} + +const styles = StyleSheet.create({ + tableContainer: { marginBottom: 12 }, + headerRow: { + flexDirection: "row", + backgroundColor: "#1a1a1a", + }, + dataRow: { flexDirection: "row" }, + rowBorder: { borderBottomWidth: 1, borderBottomColor: "#1a1a1a" }, + careerRow: { + backgroundColor: "#2a1a00", + borderTopWidth: 2, + borderTopColor: "#f59e0b", + }, + stickyCell: { + width: 60, + paddingVertical: 7, + paddingHorizontal: 8, + backgroundColor: "#000", + borderRightWidth: 1, + borderRightColor: "#333", + position: "relative", + }, + careerStickyCell: { backgroundColor: "#2a1a00" }, + cell: { + paddingVertical: 7, + paddingHorizontal: 4, + alignItems: "center", + }, + headerText: { color: "#aaa", fontSize: 10, fontWeight: "600" }, + headerLabelText: { color: "#f59e0b", fontSize: 10, fontWeight: "700" }, + labelText: { color: "#ccc", fontSize: 10, fontWeight: "600" }, + careerLabelText: { color: "#f59e0b", fontWeight: "700" }, + cellText: { color: "#ccc", fontSize: 10 }, + highlightText: { color: "#f59e0b", fontWeight: "700" }, + careerCellText: { fontWeight: "600" }, + scrollHint: { + textAlign: "right", + color: "#444", + fontSize: 9, + marginTop: 2, + paddingRight: 8, + }, +}); +``` + +- [ ] **Step 3: 型チェック + コミット** + +```bash +cd /Users/shimizuippei/projects/dev/buzzbase/mobile && yarn typecheck +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile add components/stats/PeriodToggle.tsx components/stats/StatsTable.tsx +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile commit -m "Add: PeriodToggle・StatsTable コンポーネントを追加" +``` + +--- + +## Task 10: モバイル — GameResultSummary コンポーネント群 + +**Files:** +- Create: `mobile/components/stats/WinLossCards.tsx` +- Create: `mobile/components/stats/MatchTypeBreakdown.tsx` +- Create: `mobile/components/stats/MonthlyGameChart.tsx` +- Create: `mobile/components/stats/OpponentRecord.tsx` +- Create: `mobile/components/stats/GameResultSummary.tsx` + +- [ ] **Step 1: WinLossCards** + +```typescript +// mobile/components/stats/WinLossCards.tsx +import React from "react"; +import { View, Text, StyleSheet } from "react-native"; +import type { WinLossSummary } from "../../types/stats"; + +interface WinLossCardsProps { + summary: WinLossSummary; +} + +export const WinLossCards = ({ summary }: WinLossCardsProps) => { + const total = summary.wins + summary.losses + summary.draws; + const winPct = total > 0 ? (summary.wins / total) * 100 : 0; + const lossPct = total > 0 ? (summary.losses / total) * 100 : 0; + const drawPct = total > 0 ? (summary.draws / total) * 100 : 0; + + return ( + <View> + <View style={styles.cards}> + <View style={styles.card}> + <Text style={styles.cardLabel}>勝利</Text> + <Text style={[styles.cardValue, { color: "#ef4444" }]}>{summary.wins}</Text> + </View> + <View style={styles.card}> + <Text style={styles.cardLabel}>敗北</Text> + <Text style={[styles.cardValue, { color: "#3b82f6" }]}>{summary.losses}</Text> + </View> + <View style={styles.card}> + <Text style={styles.cardLabel}>引分</Text> + <Text style={[styles.cardValue, { color: "#6b7280" }]}>{summary.draws}</Text> + </View> + </View> + <View style={styles.rateRow}> + <Text style={styles.rateLabel}>勝率</Text> + <Text style={styles.rateValue}> + {summary.win_rate.toFixed(3).replace(/^0/, "")} + </Text> + </View> + <View style={styles.bar}> + {winPct > 0 && <View style={[styles.barSegment, { width: `${winPct}%`, backgroundColor: "#ef4444" }]} />} + {lossPct > 0 && <View style={[styles.barSegment, { width: `${lossPct}%`, backgroundColor: "#3b82f6" }]} />} + {drawPct > 0 && <View style={[styles.barSegment, { width: `${drawPct}%`, backgroundColor: "#6b7280" }]} />} + </View> + </View> + ); +}; + +const styles = StyleSheet.create({ + cards: { flexDirection: "row", gap: 8, marginBottom: 12 }, + card: { + flex: 1, backgroundColor: "#1a2332", borderRadius: 8, + padding: 10, alignItems: "center", + }, + cardLabel: { fontSize: 10, color: "#888" }, + cardValue: { fontSize: 22, fontWeight: "700" }, + rateRow: { + flexDirection: "row", justifyContent: "space-between", + marginBottom: 4, + }, + rateLabel: { fontSize: 11, color: "#888" }, + rateValue: { fontSize: 11, color: "#f59e0b", fontWeight: "700" }, + bar: { + height: 6, backgroundColor: "#222", borderRadius: 3, + flexDirection: "row", overflow: "hidden", marginBottom: 16, + }, + barSegment: { height: "100%" }, +}); +``` + +- [ ] **Step 2: MatchTypeBreakdown** + +```typescript +// mobile/components/stats/MatchTypeBreakdown.tsx +import React from "react"; +import { View, Text, StyleSheet } from "react-native"; +import type { MatchTypeRecord } from "../../types/stats"; + +interface MatchTypeBreakdownProps { + breakdown: MatchTypeRecord[]; +} + +export const MatchTypeBreakdown = ({ breakdown }: MatchTypeBreakdownProps) => ( + <View> + <Text style={styles.sectionTitle}>試合種別</Text> + <View style={styles.row}> + {breakdown.map((mt) => ( + <View + key={mt.match_type} + style={[ + styles.card, + { borderLeftColor: mt.match_type === "公式戦" ? "#f59e0b" : "#6b7280" }, + ]} + > + <Text + style={[ + styles.typeLabel, + { color: mt.match_type === "公式戦" ? "#f59e0b" : "#aaa" }, + ]} + > + {mt.match_type} + </Text> + <Text style={styles.totalText}>{mt.total}試合</Text> + <Text style={styles.detailText}> + {mt.wins}勝 {mt.losses}敗 {mt.draws}分 ({mt.win_rate.toFixed(3).replace(/^0/, "")}) + </Text> + </View> + ))} + </View> + </View> +); + +const styles = StyleSheet.create({ + sectionTitle: { fontSize: 13, fontWeight: "600", color: "#ccc", marginBottom: 8 }, + row: { flexDirection: "row", gap: 8, marginBottom: 16 }, + card: { + flex: 1, backgroundColor: "#111", borderRadius: 8, + padding: 12, borderLeftWidth: 3, + }, + typeLabel: { fontSize: 11, fontWeight: "600", marginBottom: 4 }, + totalText: { fontSize: 12, color: "#ccc" }, + detailText: { fontSize: 11, color: "#888", marginTop: 2 }, +}); +``` + +- [ ] **Step 3: MonthlyGameChart** + +```typescript +// mobile/components/stats/MonthlyGameChart.tsx +import React from "react"; +import { View, Text, StyleSheet } from "react-native"; +import Svg, { Rect, Text as SvgText } from "react-native-svg"; +import type { MonthlyGame } from "../../types/stats"; + +interface MonthlyGameChartProps { + games: MonthlyGame[]; +} + +const CHART_HEIGHT = 80; +const BAR_GAP = 4; + +export const MonthlyGameChart = ({ games }: MonthlyGameChartProps) => { + if (games.length === 0) return null; + + const maxCount = Math.max(...games.map((g) => g.count), 1); + const barWidth = Math.min(30, (300 - BAR_GAP * games.length) / games.length); + + return ( + <View> + <Text style={styles.sectionTitle}>月別試合数</Text> + <View style={styles.chartContainer}> + <Svg + width={games.length * (barWidth + BAR_GAP)} + height={CHART_HEIGHT + 24} + > + {games.map((g, i) => { + const x = i * (barWidth + BAR_GAP); + const barH = (g.count / maxCount) * CHART_HEIGHT; + const y = CHART_HEIGHT - barH; + return ( + <React.Fragment key={g.month}> + <SvgText + x={x + barWidth / 2} y={y - 4} + textAnchor="middle" fill="#aaa" fontSize={9} + > + {g.count} + </SvgText> + <Rect + x={x} y={y} + width={barWidth} height={barH} + rx={3} fill="#f59e0b" + opacity={0.5 + (g.count / maxCount) * 0.5} + /> + <SvgText + x={x + barWidth / 2} y={CHART_HEIGHT + 14} + textAnchor="middle" fill="#555" fontSize={10} + > + {g.month}月 + </SvgText> + </React.Fragment> + ); + })} + </Svg> + </View> + </View> + ); +}; + +const styles = StyleSheet.create({ + sectionTitle: { fontSize: 13, fontWeight: "600", color: "#ccc", marginBottom: 8 }, + chartContainer: { alignItems: "center", marginBottom: 16 }, +}); +``` + +- [ ] **Step 4: OpponentRecord** + +```typescript +// mobile/components/stats/OpponentRecord.tsx +import React, { useState } from "react"; +import { View, Text, TouchableOpacity, StyleSheet } from "react-native"; +import type { OpponentRecord as OpponentRecordType } from "../../types/stats"; + +interface OpponentRecordProps { + records: OpponentRecordType[]; +} + +const INITIAL_SHOW = 3; + +export const OpponentRecordList = ({ records }: OpponentRecordProps) => { + const [expanded, setExpanded] = useState(false); + const displayed = expanded ? records : records.slice(0, INITIAL_SHOW); + + return ( + <View> + <Text style={styles.sectionTitle}>対戦相手別</Text> + <View style={styles.list}> + {displayed.map((r) => ( + <View key={r.team_name} style={styles.item}> + <Text style={styles.teamName} numberOfLines={1}>{r.team_name}</Text> + <Text style={[styles.stat, { color: "#ef4444" }]}>{r.wins}勝</Text> + <Text style={[styles.stat, { color: "#3b82f6" }]}>{r.losses}敗</Text> + <Text style={[styles.stat, { color: "#6b7280" }]}>{r.draws}分</Text> + </View> + ))} + </View> + {records.length > INITIAL_SHOW && ( + <TouchableOpacity onPress={() => setExpanded(!expanded)}> + <Text style={styles.toggle}> + {expanded ? "閉じる ▲" : "すべて表示 ▼"} + </Text> + </TouchableOpacity> + )} + </View> + ); +}; + +const styles = StyleSheet.create({ + sectionTitle: { fontSize: 13, fontWeight: "600", color: "#ccc", marginBottom: 8 }, + list: { gap: 6 }, + item: { + flexDirection: "row", alignItems: "center", + backgroundColor: "#111", borderRadius: 8, + paddingVertical: 10, paddingHorizontal: 12, + }, + teamName: { flex: 1, color: "#ccc", fontSize: 12 }, + stat: { fontWeight: "700", fontSize: 12, marginLeft: 12 }, + toggle: { textAlign: "center", color: "#555", fontSize: 12, paddingVertical: 8 }, +}); +``` + +- [ ] **Step 5: GameResultSummary(統合コンテナ)** + +```typescript +// mobile/components/stats/GameResultSummary.tsx +import React from "react"; +import { View, Text, StyleSheet } from "react-native"; +import { WinLossCards } from "./WinLossCards"; +import { MatchTypeBreakdown } from "./MatchTypeBreakdown"; +import { MonthlyGameChart } from "./MonthlyGameChart"; +import { OpponentRecordList } from "./OpponentRecord"; +import type { GameSummary } from "../../types/stats"; + +interface GameResultSummaryProps { + summary: GameSummary; +} + +export const GameResultSummary = ({ summary }: GameResultSummaryProps) => ( + <View style={styles.container}> + <Text style={styles.sectionHeader}>試合結果</Text> + <WinLossCards summary={summary.win_loss} /> + <MatchTypeBreakdown breakdown={summary.match_type_breakdown} /> + <MonthlyGameChart games={summary.monthly_games} /> + <OpponentRecordList records={summary.opponent_records} /> + </View> +); + +const styles = StyleSheet.create({ + container: { paddingTop: 16, borderTopWidth: 1, borderTopColor: "#222" }, + sectionHeader: { + fontSize: 15, fontWeight: "700", color: "#fff", marginBottom: 16, + }, +}); +``` + +- [ ] **Step 6: 型チェック + コミット** + +```bash +cd /Users/shimizuippei/projects/dev/buzzbase/mobile && yarn typecheck +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile add components/stats/ +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile commit -m "Add: 試合結果統計コンポーネント群を追加" +``` + +--- + +## Task 11: モバイル — StatsFilters コンポーネント + +**Files:** +- Create: `mobile/components/stats/StatsFilters.tsx` + +既存の`StatsOverview`で使われている`FilterDropdown`パターンを踏襲する。 + +- [ ] **Step 1: コンポーネント作成** + +```typescript +// mobile/components/stats/StatsFilters.tsx +import React, { useState } from "react"; +import { + View, + Text, + TouchableOpacity, + Modal, + FlatList, + StyleSheet, +} from "react-native"; +import type { StatsFilters as StatsFiltersType } from "../../types/profile"; + +interface StatsFiltersProps { + filters: StatsFiltersType; + onFiltersChange: (filters: StatsFiltersType) => void; + availableYears: number[]; + availableSeasons?: { id: string; name: string }[]; +} + +type FilterKey = "year" | "matchType" | "seasonId"; + +const MATCH_TYPES = [ + { value: undefined, label: "全て" }, + { value: "公式戦", label: "公式戦" }, + { value: "オープン戦", label: "オープン戦" }, +]; + +export const StatsFilters = ({ + filters, + onFiltersChange, + availableYears, + availableSeasons = [], +}: StatsFiltersProps) => { + const [activeDropdown, setActiveDropdown] = useState<FilterKey | null>(null); + + const yearOptions = [ + { value: undefined, label: "通算" }, + ...availableYears.map((y) => ({ value: String(y), label: `${y}年` })), + ]; + + const seasonOptions = [ + { value: undefined, label: "全シーズン" }, + ...availableSeasons.map((s) => ({ value: s.id, label: s.name })), + ]; + + const getDisplayLabel = (key: FilterKey): string => { + switch (key) { + case "year": + return filters.year ? `${filters.year}年` : "通算"; + case "matchType": + return filters.matchType || "全て"; + case "seasonId": { + const season = availableSeasons.find((s) => s.id === filters.seasonId); + return season?.name || "シーズン"; + } + } + }; + + const getOptions = (key: FilterKey) => { + switch (key) { + case "year": return yearOptions; + case "matchType": return MATCH_TYPES; + case "seasonId": return seasonOptions; + } + }; + + const handleSelect = (key: FilterKey, value: string | undefined) => { + onFiltersChange({ ...filters, [key]: value }); + setActiveDropdown(null); + }; + + const filterKeys: FilterKey[] = ["year", "matchType", "seasonId"]; + + return ( + <View style={styles.container}> + {filterKeys.map((key) => ( + <TouchableOpacity + key={key} + style={styles.filterButton} + onPress={() => setActiveDropdown(activeDropdown === key ? null : key)} + > + <Text style={styles.filterText}>{getDisplayLabel(key)} ▼</Text> + </TouchableOpacity> + ))} + + {activeDropdown && ( + <Modal transparent animationType="fade" onRequestClose={() => setActiveDropdown(null)}> + <TouchableOpacity + style={styles.overlay} + activeOpacity={1} + onPress={() => setActiveDropdown(null)} + > + <View style={styles.dropdown}> + <FlatList + data={getOptions(activeDropdown)} + keyExtractor={(item) => item.value ?? "none"} + renderItem={({ item }) => ( + <TouchableOpacity + style={styles.dropdownItem} + onPress={() => handleSelect(activeDropdown, item.value)} + > + <Text + style={[ + styles.dropdownText, + filters[activeDropdown] === item.value && styles.dropdownTextActive, + ]} + > + {item.label} + </Text> + </TouchableOpacity> + )} + /> + </View> + </TouchableOpacity> + </Modal> + )} + </View> + ); +}; + +const styles = StyleSheet.create({ + container: { flexDirection: "row", gap: 6, paddingVertical: 8 }, + filterButton: { + backgroundColor: "#111", + borderWidth: 1, + borderColor: "#333", + paddingVertical: 5, + paddingHorizontal: 10, + borderRadius: 6, + }, + filterText: { color: "#aaa", fontSize: 11 }, + overlay: { + flex: 1, backgroundColor: "rgba(0,0,0,0.5)", + justifyContent: "center", alignItems: "center", + }, + dropdown: { + backgroundColor: "#222", borderRadius: 12, + width: 200, maxHeight: 300, padding: 8, + }, + dropdownItem: { paddingVertical: 10, paddingHorizontal: 12 }, + dropdownText: { color: "#ccc", fontSize: 14 }, + dropdownTextActive: { color: "#f59e0b", fontWeight: "700" }, +}); +``` + +- [ ] **Step 2: 型チェック + コミット** + +```bash +cd /Users/shimizuippei/projects/dev/buzzbase/mobile && yarn typecheck +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile add components/stats/StatsFilters.tsx +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile commit -m "Add: 成績ページ用フィルタコンポーネントを追加" +``` + +--- + +## Task 12: モバイル — 成績ページ組み立て + タブ差し替え + +**Files:** +- Create: `mobile/app/(tabs)/stats.tsx` +- Modify: `mobile/app/(tabs)/_layout.tsx` +- Delete: `mobile/app/(tabs)/record.tsx` + +- [ ] **Step 1: 成績ページ作成** + +```typescript +// mobile/app/(tabs)/stats.tsx +import React, { useState, useCallback } from "react"; +import { + View, + Text, + ScrollView, + TouchableOpacity, + RefreshControl, + StyleSheet, + ActivityIndicator, +} from "react-native"; +import { StatsFilters } from "@components/stats/StatsFilters"; +import { SprayChart } from "@components/stats/SprayChart"; +import { PlateAppearanceDonut } from "@components/stats/PlateAppearanceDonut"; +import { StatsTable, BATTING_COLUMNS, PITCHING_COLUMNS } from "@components/stats/StatsTable"; +import { PeriodToggle } from "@components/stats/PeriodToggle"; +import { GameResultSummary } from "@components/stats/GameResultSummary"; +import { + useHitDirections, + usePlateAppearanceBreakdown, + useBattingStatsTable, + usePitchingStatsTable, + useGameSummary, +} from "@hooks/useStats"; +import type { StatsFilters as StatsFiltersType } from "../../types/profile"; +import type { StatsPeriod } from "../../types/stats"; + +type StatsTab = "batting" | "pitching"; + +export default function StatsScreen() { + const [activeTab, setActiveTab] = useState<StatsTab>("batting"); + const [filters, setFilters] = useState<StatsFiltersType>({}); + const [battingPeriod, setBattingPeriod] = useState<StatsPeriod>("yearly"); + const [pitchingPeriod, setPitchingPeriod] = useState<StatsPeriod>("yearly"); + const [tableYear, setTableYear] = useState<string | undefined>(); + + // Data hooks + const hitDirections = useHitDirections(filters); + const paBreakdown = usePlateAppearanceBreakdown(filters); + const battingTable = useBattingStatsTable(battingPeriod, battingPeriod !== "yearly" ? tableYear : undefined); + const pitchingTable = usePitchingStatsTable(pitchingPeriod, pitchingPeriod !== "yearly" ? tableYear : undefined); + const gameSummary = useGameSummary(filters.year); + + const isLoading = + hitDirections.isLoading || paBreakdown.isLoading || + battingTable.isLoading || pitchingTable.isLoading || + gameSummary.isLoading; + + const isRefreshing = + hitDirections.isRefetching || paBreakdown.isRefetching || + battingTable.isRefetching || gameSummary.isRefetching; + + const handleRefresh = useCallback(() => { + hitDirections.refetch(); + paBreakdown.refetch(); + battingTable.refetch(); + pitchingTable.refetch(); + gameSummary.refetch(); + }, [hitDirections, paBreakdown, battingTable, pitchingTable, gameSummary]); + + const handlePeriodChange = (period: StatsPeriod, tab: StatsTab) => { + if (tab === "batting") { + setBattingPeriod(period); + } else { + setPitchingPeriod(period); + } + // 月・日モードに切り替え時、年が未選択なら現在年をセット + if (period !== "yearly" && !tableYear) { + setTableYear(String(new Date().getFullYear())); + } + }; + + if (isLoading) { + return ( + <View style={styles.loadingContainer}> + <ActivityIndicator size="large" color="#d08000" /> + </View> + ); + } + + const totalPA = paBreakdown.data?.reduce((sum, cat) => sum + cat.count, 0) ?? 0; + + return ( + <ScrollView + style={styles.screen} + refreshControl={ + <RefreshControl + refreshing={isRefreshing} + onRefresh={handleRefresh} + tintColor="#d08000" + /> + } + > + {/* Tabs */} + <View style={styles.tabRow}> + <TouchableOpacity + style={[styles.tab, activeTab === "batting" && styles.tabActive]} + onPress={() => setActiveTab("batting")} + > + <Text style={[styles.tabText, activeTab === "batting" && styles.tabTextActive]}> + 打撃 + </Text> + </TouchableOpacity> + <TouchableOpacity + style={[styles.tab, activeTab === "pitching" && styles.tabActive]} + onPress={() => setActiveTab("pitching")} + > + <Text style={[styles.tabText, activeTab === "pitching" && styles.tabTextActive]}> + 投球 + </Text> + </TouchableOpacity> + </View> + + {/* Filters */} + <View style={styles.section}> + <StatsFilters + filters={filters} + onFiltersChange={setFilters} + availableYears={[2024, 2025, 2026]} + /> + </View> + + {/* Batting Tab Content */} + {activeTab === "batting" && ( + <View style={styles.section}> + {hitDirections.data && <SprayChart directions={hitDirections.data} />} + + {paBreakdown.data && ( + <PlateAppearanceDonut + breakdown={paBreakdown.data} + totalPlateAppearances={totalPA} + /> + )} + + <View style={styles.tableHeader}> + <Text style={styles.tableTitle}>打撃成績</Text> + <PeriodToggle + value={battingPeriod} + onChange={(p) => handlePeriodChange(p, "batting")} + /> + </View> + + {battingTable.data && ( + <StatsTable + rows={battingTable.data} + columns={BATTING_COLUMNS} + labelKey="label" + /> + )} + </View> + )} + + {/* Pitching Tab Content */} + {activeTab === "pitching" && ( + <View style={styles.section}> + <View style={styles.tableHeader}> + <Text style={styles.tableTitle}>投球成績</Text> + <PeriodToggle + value={pitchingPeriod} + onChange={(p) => handlePeriodChange(p, "pitching")} + /> + </View> + + {pitchingTable.data && ( + <StatsTable + rows={pitchingTable.data} + columns={PITCHING_COLUMNS} + labelKey="label" + /> + )} + </View> + )} + + {/* Game Summary (common section) */} + <View style={styles.section}> + {gameSummary.data && <GameResultSummary summary={gameSummary.data} />} + </View> + + <View style={{ height: 40 }} /> + </ScrollView> + ); +} + +const styles = StyleSheet.create({ + screen: { flex: 1, backgroundColor: "#2E2E2E" }, + loadingContainer: { + flex: 1, alignItems: "center", justifyContent: "center", + backgroundColor: "#2E2E2E", + }, + tabRow: { flexDirection: "row", gap: 4, paddingHorizontal: 16, paddingTop: 8 }, + tab: { + paddingVertical: 7, paddingHorizontal: 20, + borderRadius: 20, backgroundColor: "#222", + }, + tabActive: { backgroundColor: "#f59e0b" }, + tabText: { fontSize: 13, color: "#888" }, + tabTextActive: { color: "#000", fontWeight: "700" }, + section: { paddingHorizontal: 16 }, + tableHeader: { + flexDirection: "row", justifyContent: "space-between", + alignItems: "center", marginBottom: 8, + }, + tableTitle: { fontSize: 12, fontWeight: "600", color: "#aaa" }, +}); +``` + +- [ ] **Step 2: _layout.tsx を更新 — recordタブをstatsに差し替え** + +`mobile/app/(tabs)/_layout.tsx` のrecordタブ定義を以下に置き換え: + +```typescript +// 既存のRecordIconインポートを削除し、以下を追加(アイコンは既存のBallIconを一時流用するか、新アイコンを追加) +// import { StatsIcon } from "@components/icon/StatsIcon"; + +// Tabs.Screen name="record" のブロックを以下に置き換え: +<Tabs.Screen + name="stats" + options={{ + title: "成績", + headerStyle: { backgroundColor: "#2E2E2E" }, + headerTintColor: "#F4F4F4", + tabBarIcon: ({ color, size }) => ( + <RecordIcon size={size} color={color} /> + ), + }} +/> +``` + +注意: `RecordIcon`は一時的に流用。後日専用アイコンに差し替え可能。 +`listeners`プロパティ(game-recordへのリダイレクト)は削除。 + +- [ ] **Step 3: record.tsxを削除** + +```bash +rm /Users/shimizuippei/projects/dev/buzzbase/mobile/app/\(tabs\)/record.tsx +``` + +- [ ] **Step 4: 型チェック** + +```bash +cd /Users/shimizuippei/projects/dev/buzzbase/mobile && yarn typecheck +``` + +- [ ] **Step 5: 動作確認** + +```bash +cd /Users/shimizuippei/projects/dev/buzzbase/mobile && yarn start +``` + +iOSシミュレータで: +- タブバーに「成績」が表示される +- タップすると成績ページが表示される +- 打撃/投球タブ切り替えが動作する +- フィルタが動作する(API接続後) + +- [ ] **Step 6: コミット** + +```bash +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile add app/\(tabs\)/stats.tsx app/\(tabs\)/_layout.tsx +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile rm app/\(tabs\)/record.tsx +git -C /Users/shimizuippei/projects/dev/buzzbase/mobile commit -m "Add: 成績ページを追加し、Recordタブを差し替え" +``` + +--- + +## 実装順序の依存関係 + +``` +Task 1 (DBマイグレーション) + → Task 2 (サービス群) + → Task 3 (APIコントローラー) + → Task 4 (APIテスト) + +Task 5 (定数拡張) ← 独立して実行可能 + +Task 6 (型・サービス・フック) ← Task 3完了後 + → Task 7 (SprayChart) + → Task 8 (Donut) + → Task 9 (StatsTable) + → Task 10 (GameResultSummary) + → Task 11 (StatsFilters) + → Task 12 (ページ組み立て) ← Task 7-11すべて完了後 +``` + +バックエンド(Task 1-4)とモバイル定数(Task 5)は並列実行可能。 +モバイルUIコンポーネント(Task 7-11)も並列実行可能。 diff --git a/docs/superpowers/specs/2026-04-02-app-store-review-prompt-design.md b/docs/superpowers/specs/2026-04-02-app-store-review-prompt-design.md new file mode 100644 index 0000000..ff43db0 --- /dev/null +++ b/docs/superpowers/specs/2026-04-02-app-store-review-prompt-design.md @@ -0,0 +1,93 @@ +# App Storeレビュー促進施策 設計書 + +Issue: #196 Feature: iOSアプリのApp Storeレビュー促進施策を実装する + +## 概要 + +試合記録の完了時に、条件を満たしたユーザーへOS標準のApp Storeレビューダイアログを表示する。`expo-store-review` の `requestReview()` を使用し、カスタムUIは設けない。 + +## 表示条件 + +以下をすべて満たす場合にレビューダイアログを表示する: + +1. 累計試合記録回数がマイルストーン値(5, 20, 50, 80, 100)のいずれかに一致 +2. インストールから7日以上経過 +3. 今年の表示回数が3回未満 +4. 前回表示から90日以上経過(初回は条件スキップ) +5. `StoreReview.isAvailableAsync()` が `true` + +## データモデル + +`expo-secure-store` に以下のキーで保存: + +| キー | 型 | 説明 | +|------|------|------| +| `store_review_game_count` | string(数値) | 累計試合記録回数 | +| `store_review_install_date` | string(ISO日付) | 初回起動日 | +| `store_review_last_shown` | string(ISO日付) | 最後にレビュー依頼を表示した日 | +| `store_review_shown_count` | string(数値) | 今年の表示回数 | +| `store_review_shown_year` | string(数値) | 表示回数を管理している年 | + +## アーキテクチャ + +専用カスタムフック方式を採用。レビュー表示ロジックを `useStoreReview` フックに集約する。 + +### フックのインターフェース + +```typescript +// hooks/useStoreReview.ts +export const useStoreReview = () => { + const checkAndRequestReview: () => Promise<void> + const initInstallDate: () => Promise<void> +} +``` + +- `initInstallDate()` — 初回起動日を記録(既に記録済みの場合はスキップ) +- `checkAndRequestReview()` — 試合記録完了時に呼び出し。記録回数をインクリメントし、条件判定してレビューを表示 + +### 処理フロー + +``` +checkAndRequestReview() 呼び出し + ↓ +game_count をインクリメント & 保存 + ↓ +マイルストーン値(5, 20, 50, 80, 100)に一致するか? + → No → 終了 + ↓ Yes +install_date から7日以上経過? + → No → 終了 + ↓ Yes +shown_year が今年か確認(違えば shown_count を0にリセット) + ↓ +shown_count < 3 か? + → No → 終了 + ↓ Yes +last_shown から90日以上経過?(初回は条件スキップ) + → No → 終了 + ↓ Yes +StoreReview.isAvailableAsync() で端末対応を確認 + → No → 終了 + ↓ Yes +StoreReview.requestReview() を実行 + ↓ +last_shown, shown_count, shown_year を更新 +``` + +## 変更ファイル + +| ファイル | 変更内容 | +|---------|---------| +| `mobile/hooks/useStoreReview.ts` | 新規作成 — レビュー判定ロジック全体 | +| `mobile/app/_layout.tsx` | 修正 — `initInstallDate()` の呼び出しを追加 | +| `mobile/app/(game-record)/` 内の記録完了画面 | 修正 — 記録保存成功後に `checkAndRequestReview()` を呼び出し | + +## 追加パッケージ + +- `expo-store-review` + +## スコープ外 + +- バックエンドAPIの変更 +- カスタムダイアログ・UIの追加 +- 既存の試合記録フローの変更(完了後のコールバックに1行追加するのみ) diff --git a/docs/superpowers/specs/2026-04-03-stats-page-design.md b/docs/superpowers/specs/2026-04-03-stats-page-design.md new file mode 100644 index 0000000..4653502 --- /dev/null +++ b/docs/superpowers/specs/2026-04-03-stats-page-design.md @@ -0,0 +1,315 @@ +# 成績ページ設計書 + +## 概要 + +モバイルアプリ(mobile/)のRecordタブを差し替え、打撃・投球・試合結果の成績を一覧で確認できるページを新設する。試合記録への導線は配置しない。Web(front/)は後日対応。 + +**対応Issue:** https://github.com/ippei-shimizu/buzzbase/issues/106 + +## ページ構成: ハイブリッド型 + +上部に打撃/投球のタブ切り替え、下部に共通の試合結果統計セクション。 + +``` +┌─────────────────────────────────┐ +│ タブ: [打撃] [投球] │ +│ フィルタ: 年度 / 試合種別 / シーズン │ +├─────────────────────────────────┤ +│ ▼ 打撃タブ選択時 │ +│ 1. 打球分布図(バブルチャート) │ +│ 2. 打席結果の内訳(ドーナツ) │ +│ 3. 打撃成績表(横スクロール) │ +├─────────────────────────────────┤ +│ ▼ 投球タブ選択時 │ +│ 1. 投球成績表(横スクロール) │ +├─────────────────── ─────────────┤ +│ ▼ 共通セクション │ +│ 1. 勝敗サマリー + 勝率バー │ +│ 2. 試合種別(公式戦/オープン戦) │ +│ 3. 月別試合数(棒グラフ) │ +│ 4. 対戦相手別勝敗 │ +└─────────────────────────────────┘ +``` + +## ナビゲーション + +- タブバーのRecord(3番目)を「成績」に差し替え +- アイコン: チャート系アイコン +- 試合記録フローへの導線はこのページには配置しない + +## フィルタリング + +打球分布図・打席結果の内訳には、ダッシュボード/マイページと同様のフィルタを適用: + +- **年度**: 年度選択(通算も選択可能) +- **試合種別**: 全て / 公式戦 / オープン戦 +- **シーズン**: ユーザー定義のシーズン + +成績テーブルは年/月/日の切り替えで行の単位が変わるため、フィルタとは独立。 + +--- + +## セクション詳細 + +### 1. 打球分布図(バブルチャート) + +フィールド図上に13方向の打球データをバブルで可視化する。 + +**打球方向(13方向):** + +| # | ID | ラベル | 略称 | +|---|-----|-------|------| +| 1 | 1 | 投 | 投 | +| 2 | 2 | 捕 | 捕 | +| 3 | 3 | 一 | 一 | +| 4 | 4 | 二 | 二 | +| 5 | 5 | 三 | 三 | +| 6 | 6 | 遊 | 遊 | +| 7 | 7 | 左線(三塁線) | 左線 | +| 8 | 8 | 左 | 左 | +| 9 | 9 | 左中(左中間) | 左中 | +| 10 | 10 | 中 | 中 | +| 11 | 11 | 右中(右中間) | 右中 | +| 12 | 12 | 右 | 右 | +| 13 | 13 | 右線(一塁線) | 右線 | + +既存の9方向(投/捕/一/二/三/遊/左/中/右)の順番を維持し、4方向(左線/左中/右中/右線)を追加して統合する。既存の`batting_position_id`フィールドを拡張する形で対応。 + +**バブル表現:** +- サイズ: 打球数に比例 +- 色の濃さ: 打球数が多いほど濃い +- 色: 頻度に応じたグラデーション(多=赤、中=黄、少=青/灰) +- バブル内に打球数を数字で表示 + +**フィルタ:** 年度・試合種別・シーズンを適用 + +### 2. 打席結果の内訳(ドーナツチャート) + +打席結果を6カテゴリにグループ化してドーナツチャートで表示。 + +**カテゴリ分類:** + +| カテゴリ | 色 | 含まれる結果 | +|---------|-----|------------| +| 安打 | #ef4444(赤) | ヒット / 二塁打 / 三塁打 / 本塁打 | +| ゴロ | #6b7280(灰) | ゴロアウト | +| フライ | #3b82f6(青) | フライ / ファールフライ / ライナー | +| 三振 | #f59e0b(黄) | 三振 / 振り逃げ | +| 四死球 | #10b981(緑) | 四球 / 死球 | +| 犠打・その他 | #8b5cf6(紫) | 犠打 / 犠飛 / エラー / フィルダースチョイス | + +**表示:** +- 中央: 総打席数 +- 右横: 凡例(カテゴリ名 + 割合%) + +**フィルタ:** 年度・試合種別・シーズンを適用 + +### 3. 打撃成績表 + +NPB風の横スクロールテーブル。年/月/日の切り替えで行の単位が変わる。 + +**切り替えモード:** +- **年**: 年度別の集計行。最下部に通算行 +- **月**: 年度セレクター(◀ 2025年 ▶)→ 選択年の月別集計。最下部にその年の通算行 +- **日**: 年度セレクター → 試合単位の成績。左固定列は「日付 + 対戦相手」 + +**テーブル設計:** +- 左端列(年度/月/日付)は`position: sticky`で固定 +- 横スクロールで全カラムを閲覧可能 +- 通算行は背景色 + 上部ボーダーでハイライト(NPB風) +- 打率列はアクセントカラー(#f59e0b)で強調 + +**カラム:** + +| カラム | 説明 | +|-------|------| +| 年度(固定列) | 年 / 月 / 日付+対戦相手 | +| 打率 | ハイライト表示 | +| 試合 | | +| 打席 | | +| 打数 | | +| 安打 | | +| 二塁打 | | +| 三塁打 | | +| 本塁打 | | +| 塁打 | | +| 打点 | | +| 得点 | | +| 三振 | | +| 四球 | | +| 死球 | | +| 犠打 | | +| 犠飛 | | +| 盗塁 | | +| 盗塁死 | | +| 併殺打 | | +| 長打率 | | +| OPS | | +| ISO | | +| BB/K | | +| BABIP | | + +### 4. 投球成績表 + +打撃成績表と同じスタイル。年/月/日の切り替え + 横スクロール。 + +**カラム:** + +| カラム | 説明 | +|-------|------| +| 年度(固定列) | 年 / 月 / 日付+対戦相手 | +| 防御率 | ハイライト表示 | +| 登板 | | +| 勝利 | | +| 敗戦 | | +| ホールド | | +| セーブ | | +| 完投 | | +| 完封 | | +| 投球回 | | +| 被安打 | | +| 被本塁打 | | +| 三振 | | +| 四球 | | +| 死球 | | +| 自責点 | | +| WHIP | | +| K/9 | | +| BB/9 | | +| K/BB | | + +### 5. 試合結果統計(共通セクション) + +タブに関係なく常に表示。 + +#### 5-1. 勝敗サマリー +- 勝利 / 敗北 / 引分の3カード(大きな数字) +- 勝率プログレスバー(赤=勝 / 青=敗 / 灰=引分の比率表示) + +#### 5-2. 試合種別 +- 公式戦: 試合数、勝敗、勝率 +- オープン戦: 試合数、勝敗、勝率 +- カード形式で横並び + +#### 5-3. 月別試合数 +- 棒グラフで月ごとの試合数を表示 +- 各バーの上に試合数 +- X軸に月ラベル + +#### 5-4. 対戦相手別勝敗 +- チームごとに勝/敗/引分を1行で表示 +- 初期表示は上位3チーム +- 「すべて表示」で展開 + +--- + +## データモデル変更 + +### バックエンド(後日対応だが設計に含める) + +**plate_appearances テーブルへの打球方向フィールド追加:** + +現在の`batting_position_id`を打球方向として再利用(または新フィールド`hit_direction_id`を追加)。既存の9方向(ID: 1-9)に4方向(ID: 10-13)を追加。 + +ただし、既存の`batting_position_id`は打球が飛んだ守備位置を記録しているため、新しい4方向(三塁線/左中間/右中間/一塁線)を含む統合リストとして扱う場合は、ID体系の整理が必要。 + +**新しいID体系(提案):** + +| ID | 方向 | +|----|------| +| 1 | 投 | +| 2 | 捕 | +| 3 | 一 | +| 4 | 二 | +| 5 | 三 | +| 6 | 遊 | +| 7 | 左線(三塁線) | +| 8 | 左 | +| 9 | 左中(左中間) | +| 10 | 中 | +| 11 | 右中(右中間) | +| 12 | 右 | +| 13 | 右線(一塁線) | + +既存データ(ID 7=左, 8=中, 9=右)のマイグレーションが必要。 + +### モバイルアプリ + +- 打席記録フォーム(step2-batting)の打球方向選択肢を9→13に拡張 +- `constants/battingData.ts`の`battingResultsPositions`を更新 +- 新API(成績集計用)のサービス・型定義を追加 + +--- + +## API要件(新規) + +成績ページ用に以下のエンドポイントが必要: + +### 打球方向集計 +- `GET /api/v2/stats/hit_directions?user_id=X&year=Y&match_type=Z&season_id=W` +- レスポンス: 方向ごとの打球数 + +### 打席結果内訳 +- `GET /api/v2/stats/plate_appearance_breakdown?user_id=X&year=Y&match_type=Z&season_id=W` +- レスポンス: カテゴリごとの打席数・割合 + +### 成績テーブル(年別) +- 既存の`personal_batting_average`/`personal_pitching_result`を年度ごとの配列で返すよう拡張 +- または新エンドポイント: `GET /api/v2/stats/batting_yearly?user_id=X` + +### 成績テーブル(月別・日別) +- `GET /api/v2/stats/batting_monthly?user_id=X&year=Y` +- `GET /api/v2/stats/batting_daily?user_id=X&year=Y` + +### 試合結果統計 +- `GET /api/v2/stats/game_summary?user_id=X&year=Y` +- レスポンス: 勝敗、試合種別内訳、月別試合数、対戦相手別勝敗 + +--- + +## 技術方針 + +### チャートライブラリ +- 打球分布図: **react-native-svg** で直接描画(既存のStatsRadarChartと同じアプローチ) +- ドーナツチャート: react-native-svg で描画 +- 棒グラフ(月別試合数): react-native-svg で描画 +- 外部チャートライブラリは使わず、既存パターンに合わせる + +### 状態管理 +- React Queryでデータフェッチ(既存のhooksパターンに合わせる) +- フィルタ状態: useStateまたはZustand(既存のgameRecordStoreパターン参考) + +### コンポーネント構成 +``` +app/(tabs)/stats/ +├── index.tsx # ページルート +└── _layout.tsx # レイアウト(必要に応じて) + +components/stats/ +├── StatsPage.tsx # メインコンテナ +├── StatsTabSelector.tsx # 打撃/投球タブ +├── StatsFilters.tsx # フィルタ(年度・種別・シーズン) +├── SprayChart.tsx # 打球分布図(バブルチャート) +├── PlateAppearanceDonut.tsx # 打席結果内訳(ドーナツ) +├── BattingStatsTable.tsx # 打撃成績テーブル +├── PitchingStatsTable.tsx # 投球成績テーブル +├── PeriodToggle.tsx # 年/月/日切り替え +├── GameResultSummary.tsx # 試合結果セクション全体 +├── WinLossCards.tsx # 勝敗カード + 勝率バー +├── MatchTypeBreakdown.tsx # 試合種別 +├── MonthlyGameChart.tsx # 月別試合数棒グラフ +└── OpponentRecord.tsx # 対戦相手別勝敗 +``` + +--- + +## スコープ + +### 今回の対象 +- モバイルアプリ(mobile/)のみ +- Recordタブの差し替え +- フロントエンド実装 + 必要なバックエンドAPI + +### 今回の対象外 +- Web版(front/)への展開 +- 打球分布の詳細分析(左右投手別・状況別)※将来の課金ポイント候補 diff --git a/front b/front index 82d0b93..4bd1db0 160000 --- a/front +++ b/front @@ -1 +1 @@ -Subproject commit 82d0b9339ebd82c692b73f717f0352dc17dbf80b +Subproject commit 4bd1db0481fc215375283959133acb4fd805e90b diff --git a/mobile b/mobile index 6d32f61..f319851 160000 --- a/mobile +++ b/mobile @@ -1 +1 @@ -Subproject commit 6d32f61b822b4126afe77db322e4147bbe10e6e0 +Subproject commit f31985116497d42bf89e2b14ba71b6ee281ffdee