機能仕様書(spec.md)
プロジェクト: シュートボクシング協会 統合PWAアプリ(SB-Ticket)
バージョン: 2.0
フェーズ: Phase 2 - 設計
作成日: 2026-03-16
ソース: docs/requirements/requirements.md, non-functional.md, design-requirements.md
目次
- システム概要
- データモデル設計
- API設計
- 画面遷移図
- ビジネスロジック
- 外部API連携
- セキュリティ実装方針
1. システム概要
1-1. システムアーキテクチャ概要図
┌─────────────────────────────────────────────────────────────────┐
│ クライアント層 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ 公開ページ │ │ 管理画面 │ │ 試合LP(自動生成) │ │
│ │ (ファン向け) │ │ (協会/選手) │ │ (外部公開) │ │
│ │ ライトテーマ │ │ ダークテーマ │ │ ダークテーマ │ │
│ └──────┬───────┘ └──────┬───────┘ └──────────┬───────────┘ │
│ └──────────────────┼──────────────────────┘ │
│ │ │
│ React + TypeScript + Tailwind + shadcn/ui │
│ PWA (Service Worker) + framer-motion │
│ Vercel (ホスティング) │
└────────────────────────────┬─────────────────────────────────────┘
│ HTTPS (REST API)
│ JWT (HttpOnly Cookie)
┌────────────────────────────┼─────────────────────────────────────┐
│ API層 │
│ │ │
│ FastAPI (Python) + OpenAPI自動生成 │
│ Render (ホスティング) │
│ │ │
│ ┌─────────┐ ┌──────────┐ │ ┌──────────┐ ┌─────────────────┐ │
│ │認証 │ │RBAC │ │ │レート │ │Webhook │ │
│ │ミドル │ │ミドル │ │ │リミット │ │署名検証 │ │
│ │ウェア │ │ウェア │ │ │ミドル │ │ミドルウェア │ │
│ └─────────┘ └──────────┘ │ └──────────┘ └─────────────────┘ │
│ │ │
│ ┌─────────────────────────┼───────────────────────────────┐ │
│ │ ビジネスロジック層 │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐│ │
│ │ │試合管理│ │LP生成 │ │チケット│ │ファン │ │還元 ││ │
│ │ │サービス│ │サービス│ │サービス│ │CRM │ │計算 ││ │
│ │ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘│ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │スポン │ │キック │ │通知 │ │ランキ │ │ │
│ │ │サー │ │バック │ │サービス│ │ング │ │ │
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└────────────────────────────┬─────────────────────────────────────┘
│
┌───────────────────┼───────────────────────┐
│ │ │
┌────────┴────────┐ ┌───────┴───────┐ ┌─────────────┴─────────────┐
│ Supabase │ │ Redis │ │ 外部API │
│ │ │ (Upstash) │ │ │
│ ┌─────────────┐ │ │ ┌───────────┐ │ │ ┌──────────┐ ┌──────────┐ │
│ │ PostgreSQL │ │ │ │レート │ │ │ │Twilio │ │Claude │ │
│ │ (RLS適用) │ │ │ │リミット │ │ │ │Verify/ │ │API │ │
│ │ │ │ │ │カウンタ │ │ │ │Messaging │ │(LP生成) │ │
│ ├─────────────┤ │ │ ├───────────┤ │ │ ├──────────┤ ├──────────┤ │
│ │ Auth │ │ │ │セッション │ │ │ │チケット │ │Canvas │ │
│ │ (JWT発行) │ │ │ │キャッシュ │ │ │ │ぴあ │ │API+sharp │ │
│ ├─────────────┤ │ │ └───────────┘ │ │ │Webhook │ │(画像合成)│ │
│ │ Storage │ │ │ │ │ └──────────┘ └──────────┘ │
│ │ (画像保存) │ │ │ │ │ │
│ └─────────────┘ │ │ │ │ ┌──────────┐ │
│ │ │ │ │ │Web Push │ │
│ │ │ │ │ │API │ │
│ │ │ │ │ └──────────┘ │
└─────────────────┘ └───────────────┘ └────────────────────────────┘
1-2. 技術スタック一覧
| 層 | 技術 | 用途 |
|---|
| フロントエンド | React 18 + TypeScript | UI構築 |
| UIライブラリ | shadcn/ui + Tailwind CSS | コンポーネント・スタイリング |
| アニメーション | framer-motion | インタラクション・トランジション |
| アイコン | Lucide Icons (lucide-react) | SVGアイコン |
| PWA | Service Worker + Web Push API | オフライン・プッシュ通知 |
| フロントホスティング | Vercel | CDN配信 |
| バックエンド | FastAPI (Python 3.12+) | REST API |
| バックホスティング | Render | APIサーバー |
| データベース | Supabase (PostgreSQL 15) | データ永続化・RLS・Auth |
| ファイルストレージ | Supabase Storage | 画像・LP資材保存 |
| キャッシュ | Redis (Upstash) | レートリミット・セッション |
| SMS | Twilio Verify API / Messaging API | 二段階認証・通知 |
| AI | Claude API (Anthropic) | LP HTML自動生成 |
| 画像合成 | Canvas API + sharp (Node.js) | 対戦カードビジュアル |
| QRコード | qrcode.js | QR生成・ダウンロード |
| 暗号化 | AES-256 (Python cryptography) | 個人情報暗号化 |
1-3. ロール定義
| ロールID | ロール名 | 対象者 | 概要 |
|---|
admin | 協会管理者 | 協会スタッフ1-3名 | 全機能のフルアクセス |
player | 選手 | 登録選手(数十名規模) | 自分のデータのみ操作 |
staff | スタッフ | 協会スタッフ | 閲覧のみ(編集不可) |
concept | concept管理者 | 株式会社concept社内 | 営業リード管理・キックバック管理 |
2. データモデル設計
2-1. ER概要
players ──< event_players >── events
│ │
│ ├── lps
│ │
├── fans ──< purchases ──────┘
│
├── sponsors ──< kickbacks
│
└── player_rewards
notifications ──> players / admins
audit_logs (全テーブル操作記録)
2-2. テーブル定義
players(選手)
| カラム名 | 型 | 制約 | 説明 |
|---|
| id | UUID | PK, DEFAULT gen_random_uuid() | 選手ID |
| login_id | VARCHAR(50) | UNIQUE, NOT NULL | ログインID(英数字・協会が発行) |
| password_hash | VARCHAR(255) | NOT NULL | bcryptハッシュ(コストファクター12) |
| name | VARCHAR(100) | NOT NULL | 氏名 |
| name_kana | VARCHAR(100) | | フリガナ |
| slug | VARCHAR(100) | UNIQUE, NOT NULL | 公開ページURL用スラッグ |
| gym | VARCHAR(100) | | 所属ジム |
| weight_class | VARCHAR(50) | | 階級 |
| profile_text | TEXT | | プロフィール文 |
| profile_image_url | VARCHAR(500) | | プロフィール画像URL(Supabase Storage) |
| phone_encrypted | TEXT | | 電話番号(AES-256暗号化) |
| email_encrypted | TEXT | | メールアドレス(AES-256暗号化) |
| sns_links | JSONB | DEFAULT '{}' | SNSリンク {twitter, instagram, line, ...} |
| role | user_role | DEFAULT 'player' | ロール |
| is_locked | BOOLEAN | DEFAULT false | アカウントロック状態 |
| failed_attempts | INTEGER | DEFAULT 0 | 連続失敗回数 |
| locked_at | TIMESTAMPTZ | | ロック日時 |
| last_password_hash | VARCHAR(255) | | 前回パスワードハッシュ(再使用防止) |
| must_change_password | BOOLEAN | DEFAULT true | 初回ログイン時パスワード変更 |
| password_changed_at | TIMESTAMPTZ | | 最終パスワード変更日時 |
| phone_verified | BOOLEAN | DEFAULT false | 電話番号認証済フラグ |
| push_subscription | JSONB | | Web Push購読情報 |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | 作成日時 |
| updated_at | TIMESTAMPTZ | DEFAULT NOW() | 更新日時 |
| deleted_at | TIMESTAMPTZ | | 論理削除日時 |
インデックス: idx_players_login_id (login_id), idx_players_slug (slug)
admins(管理者 - 協会管理者・スタッフ・concept管理者)
| カラム名 | 型 | 制約 | 説明 |
|---|
| id | UUID | PK, DEFAULT gen_random_uuid() | 管理者ID |
| login_id | VARCHAR(50) | UNIQUE, NOT NULL | ログインID |
| password_hash | VARCHAR(255) | NOT NULL | bcryptハッシュ |
| name | VARCHAR(100) | NOT NULL | 氏名 |
| phone_encrypted | TEXT | | 電話番号(AES-256暗号化) |
| role | user_role | NOT NULL, CHECK (role IN ('admin', 'staff', 'concept')) | 'admin' / 'staff' / 'concept' |
| is_locked | BOOLEAN | DEFAULT false | アカウントロック状態 |
| failed_attempts | INTEGER | DEFAULT 0 | 連続失敗回数 |
| locked_at | TIMESTAMPTZ | | ロック日時 |
| last_password_hash | VARCHAR(255) | | 前回パスワードハッシュ |
| must_change_password | BOOLEAN | DEFAULT true | 初回ログイン時パスワード変更 |
| phone_verified | BOOLEAN | DEFAULT false | 電話番号認証済フラグ |
| push_subscription | JSONB | | Web Push購読情報 |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
| updated_at | TIMESTAMPTZ | DEFAULT NOW() | |
| deleted_at | TIMESTAMPTZ | | |
events(試合・イベント)
| カラム名 | 型 | 制約 | 説明 |
|---|
| id | UUID | PK, DEFAULT gen_random_uuid() | 試合ID |
| name | VARCHAR(100) | NOT NULL | 試合名 |
| event_date | DATE | NOT NULL | 開催日 |
| venue | VARCHAR(100) | NOT NULL | 会場名 |
| door_open_time | TIME | | 開場時間 |
| start_time | TIME | NOT NULL | 開始時間 |
| ticket_sale_date | DATE | NOT NULL | チケット発売日 |
| ticket_deadline_date | DATE | NOT NULL | チケット販売締切日 |
| seat_types | JSONB | NOT NULL | 席種・定価 [{name, price}] |
| sales_channels | TEXT[] | NOT NULL | 販売経路 ['pia','eplus','cash'] |
| pia_base_url | TEXT | | チケットぴあ基底URL |
| description | TEXT | | 試合紹介テキスト |
| promo_images | TEXT[] | DEFAULT '{}' | 宣材写真URL(複数) |
| status | VARCHAR(20) | DEFAULT 'upcoming' | 'upcoming' / 'ongoing' / 'completed' / 'cancelled' |
| created_by | UUID | FK → admins(id) | 作成者 |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
| updated_at | TIMESTAMPTZ | DEFAULT NOW() | |
| deleted_at | TIMESTAMPTZ | | |
インデックス: idx_events_date (event_date), idx_events_status (status)
event_players(試合×選手 中間テーブル)
| カラム名 | 型 | 制約 | 説明 |
|---|
| id | UUID | PK, DEFAULT gen_random_uuid() | |
| event_id | UUID | FK → events(id), NOT NULL | 試合ID |
| player_id | UUID | FK → players(id), NOT NULL | 選手ID |
| quota | INTEGER | DEFAULT 0 | ノルマ枚数 |
| promo_images | TEXT[] | | 宣材写真URL(複数) |
| ref_code | VARCHAR(100) | UNIQUE, NOT NULL | ref付きURLコード (例: sb_player007_event001) |
| pia_url | VARCHAR(500) | | ぴあ ref付き完全URL |
| eplus_url | VARCHAR(500) | | イープラス URL |
| reward_rate | DECIMAL(5,2) | DEFAULT 0 | LP経由売上の還元率(%) |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
ユニーク制約: uq_event_player (event_id, player_id)
インデックス: idx_ep_ref_code (ref_code)
tickets(チケット販売実績)
| カラム名 | 型 | 制約 | 説明 |
|---|
| id | UUID | PK, DEFAULT gen_random_uuid() | |
| event_id | UUID | FK → events(id), NOT NULL | 試合ID |
| player_id | UUID | FK → players(id) | 担当選手(NULL=協会枠) |
| fan_id | UUID | FK → fans(id) | 購入ファン(特定可能な場合) |
| seat_type | VARCHAR(50) | NOT NULL | 席種名 |
| quantity | INTEGER | NOT NULL, CHECK > 0 | 購入枚数 |
| unit_price | INTEGER | NOT NULL | 単価(円) |
| total_amount | INTEGER | NOT NULL | 合計金額 |
| route | ticket_route | NOT NULL | 販売経路(下記ENUM参照) |
| buyer_name | VARCHAR(100) | | 購入者名 |
| payment_method | payment_method | | 支払方法 ('cash'/'transfer'/'other') |
| is_paid | BOOLEAN | DEFAULT false | 支払済フラグ |
| paid_at | TIMESTAMPTZ | | 支払日時 |
| memo | VARCHAR(200) | | 備考 |
| webhook_payload | JSONB | | ぴあWebhook生データ(検証用) |
| purchased_at | TIMESTAMPTZ | DEFAULT NOW() | 購入日時 |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
| updated_at | TIMESTAMPTZ | DEFAULT NOW() | |
route ENUM値:
player_pia — 選手QR/URL経由(ぴあ)
player_eplus — 選手URL経由(イープラス)
association_lp — 協会LP経由(ぴあ)
player_cash — 選手手売り(現金)
player_transfer — 選手手売り(振込)
day_ticket — 当日券
インデックス: idx_tickets_event (event_id), idx_tickets_player (player_id), idx_tickets_route (route)
purchases(購入履歴 - ファンCRM用ビュー)
注記: tickets テーブルがマスタデータ。purchases はファンCRM用に tickets を fan_id 軸で集約するビュー/マテリアライズドビューとして実装する。
CREATE VIEW purchases AS
SELECT
t.id AS purchase_id,
t.fan_id,
t.player_id,
t.event_id,
t.quantity,
t.route,
t.is_paid,
t.purchased_at
FROM tickets t
WHERE t.fan_id IS NOT NULL;
fans(ファン - 購入者DB)
| カラム名 | 型 | 制約 | 説明 |
|---|
| id | UUID | PK, DEFAULT gen_random_uuid() | ファンID |
| player_id | UUID | FK → players(id), NOT NULL | 紐づく選手 |
| name | VARCHAR(100) | | 購入者名 |
| phone_encrypted | BYTEA | | 電話番号(AES-256暗号化) |
| email_encrypted | BYTEA | | メール(AES-256暗号化) |
| memo | TEXT | | 選手メモ欄 |
| last_purchase_event_id | UUID | FK → events(id) | 最終購入試合ID |
| last_purchase_at | TIMESTAMPTZ | | 最終購入日時 |
| total_purchases | INTEGER | DEFAULT 0 | 累計購入枚数 |
| is_dormant | BOOLEAN | DEFAULT false | 掘り起こし対象フラグ |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
| updated_at | TIMESTAMPTZ | DEFAULT NOW() | |
| deleted_at | TIMESTAMPTZ | | |
インデックス: idx_fans_player (player_id), idx_fans_dormant (is_dormant, player_id)
sponsors(スポンサー企業)
| カラム名 | 型 | 制約 | 説明 |
|---|
| id | UUID | PK, DEFAULT gen_random_uuid() | |
| player_id | UUID | FK → players(id), NOT NULL | 紐づく選手 |
| company_name | VARCHAR(200) | NOT NULL | 企業名 |
| industry | VARCHAR(100) | | 業種 |
| contact_name | VARCHAR(100) | NOT NULL | 担当者名 |
| contact_phone_encrypted | BYTEA | NOT NULL | 担当者電話(暗号化) |
| contact_email_encrypted | BYTEA | | 担当者メール(暗号化) |
| logo_url | VARCHAR(500) | | ロゴ画像URL |
| website_url | VARCHAR(500) | | 企業サイトURL |
| contract_amount | INTEGER | | 契約金額(円) |
| contract_start_date | DATE | | 契約開始日 |
| contract_end_date | DATE | | 契約終了日 |
| concept_sales_status | concept_status | DEFAULT 'untouched' | concept営業ステータス |
| concept_memo | TEXT | | concept内部メモ |
| is_public_to_association | BOOLEAN | DEFAULT false | 協会への公開許可フラグ |
| is_approved_for_display | BOOLEAN | DEFAULT false | 公開ページ掲載承認フラグ |
| memo | TEXT | | 備考 |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
| updated_at | TIMESTAMPTZ | DEFAULT NOW() | |
| deleted_at | TIMESTAMPTZ | | |
concept_sales_status ENUM値: untouched / approached / negotiating / closed / passed
インデックス: idx_sponsors_player (player_id), idx_sponsors_concept_status (concept_sales_status)
kickbacks(キックバック)
| カラム名 | 型 | 制約 | 説明 |
|---|
| id | UUID | PK, DEFAULT gen_random_uuid() | |
| sponsor_id | UUID | FK → sponsors(id), NOT NULL | 紹介元スポンサー |
| player_id | UUID | FK → players(id) | 紹介した選手(任意) |
| contract_amount | INTEGER | NOT NULL | concept AGI受注金額(円) |
| kickback_rate | DECIMAL(5,2) | NOT NULL | キックバック割合(%) |
| kickback_amount | INTEGER | GENERATED ALWAYS AS (FLOOR(contract_amount * kickback_rate / 100)) STORED | 自動計算: contract_amount * kickback_rate / 100 |
| payment_due_date | DATE | | 支払予定日 |
| is_paid | BOOLEAN | DEFAULT false | 支払済フラグ |
| paid_date | DATE | | 実際の支払日 |
| transfer_info_encrypted | BYTEA | | 振込先情報(暗号化) |
| memo | TEXT | | 備考 |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
| updated_at | TIMESTAMPTZ | DEFAULT NOW() | |
インデックス: idx_kickbacks_sponsor (sponsor_id), idx_kickbacks_paid (is_paid)
lps(試合LP - ランディングページ)
| カラム名 | 型 | 制約 | 説明 |
|---|
| id | UUID | PK, DEFAULT gen_random_uuid() | |
| event_id | UUID | FK → events(id), NOT NULL, UNIQUE | 試合ID(1試合1LP) |
| status | lp_status | DEFAULT 'draft' | 'draft' / 'reviewing' / 'published' / 'unpublished' |
| html_content | TEXT | | 生成済みLP HTML |
| hero_image_url | TEXT | | ヒービジュアル画像URL |
| ogp_image_url | TEXT | | OGP画像URL (1200x630) |
| meta_title | VARCHAR(200) | | metaタイトル |
| meta_description | TEXT | | metaディスクリプション |
| association_ref_code | VARCHAR(100) | UNIQUE | 協会枠refコード (例: sb_association_event001) |
| association_pia_url | TEXT | | 協会枠ぴあURL |
| editor_data | JSONB | | ビジュアルエディタ保存データ |
| generated_by_model | VARCHAR(100) | | 生成に使用したAIモデル名 |
| published_at | TIMESTAMPTZ | | 公開日時 |
| published_by | UUID | FK → admins(id) | 公開許可した管理者 |
| unpublished_at | TIMESTAMPTZ | | 非公開日時 |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
| updated_at | TIMESTAMPTZ | DEFAULT NOW() | |
インデックス: idx_lps_event (event_id), idx_lps_status (status)
player_rewards(選手還元 - LP経由売上の還元管理)
| カラム名 | 型 | 制約 | 説明 |
|---|
| id | UUID | PK, DEFAULT gen_random_uuid() | |
| event_id | UUID | FK → events(id), NOT NULL | 試合ID |
| player_id | UUID | FK → players(id), NOT NULL | 選手ID |
| lp_sales_amount | INTEGER | DEFAULT 0 | LP経由売上金額 |
| reward_rate | DECIMAL(5,2) | NOT NULL | 還元率(%) |
| reward_amount | INTEGER | GENERATED ALWAYS AS (FLOOR(lp_sales_amount * reward_rate / 100)) STORED | 還元額 = lp_sales_amount * reward_rate / 100 |
| status | VARCHAR(20) | DEFAULT 'pending' | 'pending' / 'confirmed' / 'paid' |
| payment_due_date | DATE | | 支払予定日 |
| paid_date | DATE | | 実際の支払日 |
| confirmed_at | TIMESTAMPTZ | | 確定日時 |
| confirmed_by | UUID | FK → admins(id) | 確定した管理者 |
| memo | TEXT | | 備考 |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
| updated_at | TIMESTAMPTZ | DEFAULT NOW() | |
ユニーク制約: uq_reward_event_player (event_id, player_id)
notifications(通知)
| カラム名 | 型 | 制約 | 説明 |
|---|
| id | UUID | PK, DEFAULT gen_random_uuid() | |
| recipient_type | user_role | NOT NULL | 'player' / 'admin' |
| recipient_id | UUID | NOT NULL | 送信先のplayer_id or admin_id |
| type | notification_type | NOT NULL | 通知種別(下記参照) |
| method | notification_method | NOT NULL | 通知方法: 'sms' / 'push' / 'both' |
| title | VARCHAR(200) | NOT NULL | 通知タイトル |
| body | TEXT | NOT NULL | 通知本文 |
| related_event_id | UUID | | 関連試合ID |
| related_player_id | UUID | | 関連選手ID |
| is_read | BOOLEAN | DEFAULT false | 既読フラグ |
| is_sent | BOOLEAN | DEFAULT false | 送信済フラグ |
| sent_at | TIMESTAMPTZ | | 送信日時 |
| sms_sid | VARCHAR(100) | | Twilio SMS SID |
| error_message | TEXT | | 送信エラー内容 |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
通知type一覧(notification_type ENUM):
purchase — チケット購入通知
deadline — 締切通知
lp_published — LP公開通知
reward_confirmed — 還元額確定通知
reward_paid — 還元支払完了通知
通知method一覧(notification_method ENUM):
sms — SMS通知
push — プッシュ通知
both — SMS + プッシュ通知
インデックス: idx_notifications_recipient (recipient_type, recipient_id), idx_notifications_read (is_read)
audit_logs(操作ログ)
| カラム名 | 型 | 制約 | 説明 |
|---|
| id | UUID | PK, DEFAULT gen_random_uuid() | |
| actor_type | VARCHAR(10) | NOT NULL | 'player' / 'admin' / 'system' |
| actor_id | UUID | | 操作者ID |
| action | VARCHAR(50) | NOT NULL | 操作種別 |
| resource_type | VARCHAR(50) | NOT NULL | 対象テーブル名 |
| resource_id | UUID | | 対象レコードID |
| changes | JSONB | | 変更内容 {before, after} |
| ip_address | INET | | IPアドレス |
| user_agent | TEXT | | ユーザーエージェント |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
action一覧(主要):
create, update, delete
login, login_failed, logout, account_locked, account_unlocked
password_changed, password_reset
lp_generated, lp_published, lp_suspended
reward_confirmed, reward_paid
webhook_received, webhook_verified, webhook_rejected
concept_status_changed, concept_public_toggled
インデックス: idx_audit_actor (actor_type, actor_id), idx_audit_resource (resource_type, resource_id), idx_audit_created (created_at)
page_views(ページ閲覧数 - ランキング用)
| カラム名 | 型 | 制約 | 説明 |
|---|
| id | UUID | PK, DEFAULT gen_random_uuid() | |
| page_type | VARCHAR(20) | NOT NULL | 'player_page' / 'lp' |
| player_id | UUID | FK → players(id) | 選手ID(選手ページの場合) |
| event_id | UUID | FK → events(id) | 試合ID(LPの場合) |
| viewed_at | DATE | NOT NULL | 閲覧日 |
| count | INTEGER | DEFAULT 1 | 日次集計カウント |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
ユニーク制約: uq_pageview (page_type, player_id, event_id, viewed_at)
インデックス: idx_pv_date (viewed_at), idx_pv_player (player_id)
sms_verification_codes(SMS認証コード - 一時テーブル)
| カラム名 | 型 | 制約 | 説明 |
|---|
| id | UUID | PK | |
| user_type | VARCHAR(10) | NOT NULL | 'player' / 'admin' |
| user_id | UUID | NOT NULL | |
| code_hash | VARCHAR(255) | NOT NULL | 6桁コードのハッシュ |
| attempts | INTEGER | DEFAULT 0 | 入力試行回数 |
| expires_at | TIMESTAMPTZ | NOT NULL | 有効期限(5分後) |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
インデックス: idx_sms_user (user_type, user_id)
3. API設計
3-0. 共通仕様
ベースURL: https://api.sb-ticket.com/api/v1
リクエストヘッダー:
Content-Type: application/json
Authorization: Bearer <access_token> ※認証が必要なAPI
共通レスポンス形式:
{
"success": true,
"data": { ... },
"meta": {
"page": 1,
"per_page": 20,
"total": 100,
"total_pages": 5
}
}
エラーレスポンス形式:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "入力値が不正です",
"details": [
{ "field": "name", "message": "必須項目です" }
]
}
}
ページネーション: ?page=1&per_page=20
ソート: ?sort=created_at&order=desc
レートリミット:
- 認証API: 10回/分/IP
- 通常API: 100回/分/ユーザー
- Webhook: 1000回/分
3-1. 認証API(/api/v1/auth)
| メソッド | パス | 説明 | 認可 | リクエスト概要 | レスポンス概要 |
|---|
| POST | /auth/login | ログイン(Step1: ID+パスワード) | なし | {login_id, password} | {requires_sms: true, user_type} |
| POST | /auth/verify-sms | SMS認証コード検証(Step2) | なし | {login_id, code} | {access_token, refresh_token, user} |
| POST | /auth/refresh | トークンリフレッシュ | リフレッシュトークン | {refresh_token} | {access_token, refresh_token} |
| POST | /auth/logout | ログアウト | 全ロール | (なし) | {success: true} |
| POST | /auth/change-password | パスワード変更 | 全ロール | {current_password, new_password} | {success: true} |
| POST | /auth/force-change-password | 初回パスワード強制変更 | 全ロール(初回) | {new_password, phone} | {requires_sms: true} |
| POST | /auth/forgot-password | パスワードリセット要求 | なし | {login_id} | {message: "SMSを送信しました"} |
| POST | /auth/reset-password | パスワードリセット実行 | なし | {login_id, code, new_password} | {success: true} |
| POST | /auth/unlock-account | アカウントロック解除 | admin | {user_type, user_id} | {success: true} |
3-2. 試合管理API(/api/v1/events)
| メソッド | パス | 説明 | 認可 | リクエスト概要 | レスポンス概要 |
|---|
| GET | /events | 試合一覧取得 | 全ロール | ?status=upcoming&page=1 | [{id, name, event_date, venue, status, ...}] |
| GET | /events/:id | 試合詳細取得 | 全ロール | — | {event, players, seat_types, lp_status} |
| POST | /events | 試合作成 | admin | {name, event_date, venue, ...} | {event} |
| PUT | /events/:id | 試合編集 | admin | {name, event_date, ...} | {event} |
| DELETE | /events/:id | 試合論理削除 | admin | — | {success: true} |
| POST | /events/:id/players | 出場選手アサイン | admin | {player_ids: [...]} | {event_players} |
| PUT | /events/:id/players/:player_id | 選手設定更新(ノルマ・還元率) | admin | {quota, reward_rate} | {event_player} |
| DELETE | /events/:id/players/:player_id | 出場選手削除 | admin | — | {success: true} |
| POST | /events/:id/players/bulk-quota | ノルマ一括設定 | admin | {quota: 30} or {quotas: [{player_id, quota}]} | {event_players} |
| POST | /events/:id/promo-images | 宣材写真アップロード | admin | multipart/form-data (images[]) | {urls: [...]} |
| GET | /events/:id/sales-summary | 試合別販売サマリー | admin, staff | — | {total, by_player, by_route, by_seat_type} |
| GET | /events/public | 公開用試合一覧 | なし(公開) | ?status=upcoming | [{id, name, event_date, venue}] |
| GET | /events/:id/public | 公開用試合詳細 | なし(公開) | — | {event, players_summary} |
3-3. チケット販売API(/api/v1/tickets)
| メソッド | パス | 説明 | 認可 | リクエスト概要 | レスポンス概要 |
|---|
| GET | /tickets | チケット販売一覧 | admin, staff | ?event_id=&player_id=&route= | [{ticket}] |
| GET | /tickets/my | 自分のチケット販売一覧 | player | ?event_id= | [{ticket}] |
| POST | /tickets/manual | 手売り登録 | player, admin | {event_id, player_id, buyer_name, quantity, seat_type, payment_method, is_paid} | {ticket} |
| PUT | /tickets/:id | チケット情報更新 | player, admin | {buyer_name, is_paid, paid_at, memo} | {ticket} |
| DELETE | /tickets/:id | チケット削除 | admin | — | {success: true} |
| POST | /tickets/day-tickets | 当日券一括入力 | admin | [{event_id, quantity, seat_type}] | {tickets} |
| POST | /tickets/webhook/pia | チケットぴあWebhookコールバック | Webhook署名検証 | {ref, buyer_info, seat_type, quantity, ...} | {success: true} |
| GET | /tickets/my/stats | 自分の販売統計 | player | ?event_id= | {total_sold, quota, achievement_rate, by_route} |
| GET | /tickets/export | チケットデータCSV出力 | admin | ?event_id= | CSV file |
3-4. 選手API(/api/v1/players)
| メソッド | パス | 説明 | 認可 | リクエスト概要 | レスポンス概要 |
|---|
| GET | /players | 選手一覧(管理用) | admin, staff | ?page=1&search= | [{player_summary}] |
| GET | /players/:id | 選手詳細(管理用) | admin, staff | — | {player, stats, fans_count, sponsors} |
| POST | /players | 選手アカウント作成 | admin | {login_id, name, phone, initial_password, ...} | {player} |
| PUT | /players/:id | 選手情報更新(管理) | admin | {name, gym, weight_class, ...} | {player} |
| DELETE | /players/:id | 選手論理削除 | admin | — | {success: true} |
| GET | /players/me | 自分のプロフィール取得 | player | — | {player} |
| PUT | /players/me | 自分のプロフィール更新 | player | {profile_text, sns_links, ...} | {player} |
| POST | /players/me/profile-image | プロフィール画像アップロード | player | multipart/form-data | {image_url} |
| GET | /players/me/dashboard | 選手ホームダッシュボード | player | — | {upcoming_events, achievement, income_summary} |
| GET | /players/me/income | 収入サマリー | player | — | {personal_sales, lp_rewards_confirmed, lp_rewards_estimate, total_received, next_payment_date} |
| GET | /players/me/qr/:event_id | QRコード生成 | player | ?size=standard&format=png | PNG画像 |
| GET | /players/me/qr/:event_id/instagram | Instagram用QR画像生成 | player | — | PNG画像 (1080x1920) |
| GET | /players/public | 公開用選手一覧 | なし(公開) | — | [{name, slug, gym, weight_class, profile_image_url}] |
| GET | /players/public/:slug | 公開用選手詳細 | なし(公開) | — | {player_public, next_event, sponsors, ticket_urls} |
3-5. LP生成API(/api/v1/lps)
| メソッド | パス | 説明 | 認可 | リクエスト概要 | レスポンス概要 |
|---|
| GET | /lps | LP一覧 | admin, staff | ?status=&event_id= | [{lp_summary}] |
| GET | /lps/:id | LP詳細 | admin, staff | — | {lp, event, editor_data} |
| POST | /lps/generate | LP自動生成 | admin | {event_id} | {lp, job_id} ※非同期 |
| GET | /lps/generate/:job_id/status | LP生成ジョブ状態確認 | admin | — | {status: 'processing'/'completed'/'failed', progress} |
| PUT | /lps/:id | LP内容更新(エディタ保存) | admin | {editor_data, html_content} | {lp} |
| POST | /lps/:id/publish | LP公開 | admin | — | {lp, published_url} |
| POST | /lps/:id/suspend | LP公開停止 | admin | — | {lp} |
| POST | /lps/:id/regenerate | LP再生成 | admin | — | {lp, job_id} |
| POST | /lps/:id/hero-image | ヒービジュアル再合成 | admin | {player_ids, layout} | {hero_image_url} |
| GET | /lps/public/:event_id | 公開LP表示 | なし(公開) | — | HTML or {lp_public_data} |
| GET | /lps/:id/preview | LPプレビュー | admin | ?device=pc/mobile | {preview_html} |
3-6. ファンCRM API(/api/v1/fans)
| メソッド | パス | 説明 | 認可 | リクエスト概要 | レスポンス概要 |
|---|
| GET | /fans | 自分のファン一覧 | player | ?is_dormant=true&search=&page=1 | [{fan, purchase_history}] |
| GET | /fans/:id | ファン詳細 | player(自分のファンのみ) | — | {fan, purchases, events} |
| POST | /fans | ファン手動登録 | player | {name, phone, email, memo} | {fan} |
| PUT | /fans/:id | ファン情報更新 | player(自分のファンのみ) | {name, phone, email, memo} | {fan} |
| DELETE | /fans/:id | ファン論理削除 | player(自分のファンのみ) | — | {success: true} |
| GET | /fans/dormant | 掘り起こし対象ファン一覧 | player | — | [{fan, last_event, days_since}] |
| POST | /fans/:id/share-url | 掘り起こし用URL生成 | player | {event_id} | {url, qr_url} |
| GET | /fans/admin | 全ファン一覧(管理用) | admin | ?player_id=&page=1 | [{fan}] |
| GET | /fans/export | ファンデータCSV出力 | admin | ?player_id= | CSV file |
3-7. スポンサーAPI(/api/v1/sponsors)
| メソッド | パス | 説明 | 認可 | リクエスト概要 | レスポンス概要 |
|---|
| GET | /sponsors | スポンサー一覧 | admin, staff | ?player_id=&status= | [{sponsor}] |
| GET | /sponsors/:id | スポンサー詳細 | admin, staff, concept | — | {sponsor, kickbacks} |
| POST | /sponsors | スポンサー登録 | admin | {player_id, company_name, industry, contact_name, contact_phone, ...} | {sponsor} |
| PUT | /sponsors/:id | スポンサー情報更新 | admin | {...} | {sponsor} |
| DELETE | /sponsors/:id | スポンサー論理削除 | admin | — | {success: true} |
| POST | /sponsors/:id/logo | ロゴ画像アップロード | admin | multipart/form-data | {logo_url} |
| POST | /sponsors/:id/approve-display | 公開ページ掲載承認 | admin | — | {sponsor} |
| POST | /sponsors/:id/revoke-display | 公開ページ掲載取消 | admin | — | {sponsor} |
| GET | /sponsors/public/:player_slug | 選手の公開スポンサー一覧 | なし(公開) | — | [{company_name, logo_url, website_url}] |
3-8. キックバックAPI(/api/v1/kickbacks)
| メソッド | パス | 説明 | 認可 | リクエスト概要 | レスポンス概要 |
|---|
| GET | /kickbacks | キックバック一覧 | admin, concept | ?sponsor_id=&is_paid= | [{kickback}] |
| GET | /kickbacks/:id | キックバック詳細 | admin, concept | — | {kickback, sponsor} |
| POST | /kickbacks | キックバック登録 | admin, concept | {sponsor_id, player_id, contract_amount, kickback_rate, payment_due_date} | {kickback} |
| PUT | /kickbacks/:id | キックバック更新 | admin, concept | {...} | {kickback} |
| POST | /kickbacks/:id/mark-paid | 支払済マーク | admin, concept | {paid_date} | {kickback} |
| GET | /kickbacks/summary | キックバックサマリー | admin, concept | — | {total, unpaid, by_sponsor} |
| GET | /kickbacks/export | CSV出力 | admin, concept | — | CSV file |
3-9. 通知API(/api/v1/notifications)
| メソッド | パス | 説明 | 認可 | リクエスト概要 | レスポンス概要 |
|---|
| GET | /notifications | 自分の通知一覧 | 全ロール | ?is_read=false&page=1 | [{notification}] |
| PUT | /notifications/:id/read | 既読マーク | 全ロール | — | {notification} |
| PUT | /notifications/read-all | 全既読 | 全ロール | — | {count} |
| GET | /notifications/unread-count | 未読件数 | 全ロール | — | {count} |
| POST | /notifications/send | 通知送信(管理用) | admin | {recipient_type, recipient_id, title, body, channel} | {notification} |
| POST | /notifications/subscribe-push | Web Push購読登録 | 全ロール | {subscription} | {success: true} |
3-10. ランキングAPI(/api/v1/rankings)
| メソッド | パス | 説明 | 認可 | リクエスト概要 | レスポンス概要 |
|---|
| GET | /rankings/tickets | 試合別チケット販売ランキング | 全ロール | ?event_id= | [{rank, player_name, count}] ※選手は順位のみ |
| GET | /rankings/daily | デイリーランキング | 全ロール | ?date= | [{rank, player_name, count}] |
| GET | /rankings/page-views | 選手ページPVランキング | admin | ?period=daily/weekly/monthly | [{rank, player, pv_count}] |
| GET | /rankings/lp-sales | LP経由販売ランキング | admin | ?event_id= | [{rank, player, lp_count}] |
3-11. 管理者API(/api/v1/admin)
| メソッド | パス | 説明 | 認可 | リクエスト概要 | レスポンス概要 |
|---|
| GET | /admin/dashboard | 協会ダッシュボード | admin, staff | — | {kpi, upcoming_events, alerts, lp_statuses} |
| GET | /admin/rewards | 還元管理一覧 | admin | ?event_id=&status= | [{player_reward}] |
| POST | /admin/rewards/:id/confirm | 還元額確定 | admin | — | {player_reward} |
| POST | /admin/rewards/:id/mark-paid | 還元支払済マーク | admin | {paid_date} | {player_reward} |
| POST | /admin/rewards/batch-confirm | 還元一括確定(試合終了後) | admin | {event_id} | {confirmed_count} |
| GET | /admin/rewards/export | 還元データCSV出力 | admin | ?event_id= | CSV file |
| GET | /admin/audit-logs | 操作ログ一覧 | admin, concept | ?actor_type=&action=&from=&to=&page=1 | [{audit_log}] |
| POST | /admin/accounts | 管理者アカウント作成 | admin | {login_id, name, phone, role, initial_password} | {admin} |
| GET | /admin/health | ヘルスチェック | なし | — | {status, db, redis, twilio, claude} |
3-12. concept管理API(/api/v1/concept)
| メソッド | パス | 説明 | 認可 | リクエスト概要 | レスポンス概要 |
|---|
| GET | /concept/leads | 営業リード一覧 | concept | ?status=&page=1 | [{sponsor, sales_status}] |
| PUT | /concept/leads/:sponsor_id | 営業ステータス更新 | concept | {concept_sales_status, concept_memo} | {sponsor} |
| POST | /concept/leads/:sponsor_id/publish | 協会への公開許可 | concept | — | {sponsor} |
| POST | /concept/leads/:sponsor_id/unpublish | 協会への公開取消 | concept | — | {sponsor} |
| GET | /concept/kickbacks | 自社キックバック一覧 | concept | ?is_paid= | [{kickback}] |
| GET | /concept/dashboard | conceptダッシュボード | concept | — | {leads_summary, kickbacks_summary} |
4. 画面遷移図
4-1. 全体画面遷移図
┌──────────────────────────┐
│ トップ(公開) │ ← 未ログインユーザー
│ / │
└──┬────────┬───────────┬───┘
│ │ │
┌──────────┘ │ └──────────┐
▼ ▼ ▼
┌───────────────────┐ ┌─────────────────┐ ┌────────────────────┐
│ 選手個別公開ページ │ │ 試合LP(公開) │ │ ログイン │
│ /fighters/:slug │ │ /events/:id │ │ /login │
└───────────────────┘ └─────────────────┘ └──────┬─────────────┘
│
┌──────────────────┤
│ (初回ログイン) │ (通常ログイン)
▼ │
┌──────────────────┐ │
│ 初回パスワード変更 │ │
│ /change-password │ │
└────────┬─────────┘ │
│ (SMS認証後) │ (SMS認証後)
└──────────┬──────────┘
│
┌───────────────────┬┴──────────────────────┐
│ player │ admin/staff │ concept
▼ ▼ ▼
┌─────────────────┐ ┌──────────────────┐ ┌────────────────────┐
│ 選手ホーム │ │ 協会ダッシュボード │ │ concept営業リード │
│ /dashboard │ │ /admin │ │ /concept │
└──┬──┬──┬──┬──┬──┘ └┬──┬──┬──┬──┬──┬──┘ └──┬──┬─────────────┘
│ │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │
4-2. 選手画面遷移(/dashboard配下)
選手ホーム (/dashboard)
├── ファンCRM (/dashboard/fans)
│ ├── ファン詳細(スライドオーバー)
│ └── 掘り起こしリスト (/dashboard/fans/dormant)
│ └── URL生成・SNSシェア
│
├── QR・URL生成 (/dashboard/qr)
│ └── 試合選択 → QR表示 → ダウンロード / SNSシェア
│
├── 手売り入力 (/dashboard/manual-sales)
│ └── 購入登録フォーム
│
├── 公開ページ編集 (/dashboard/profile)
│ └── プロフィール画像・SNSリンク・スポンサー表示管理
│
├── 収入サマリー (/dashboard/income)
│ └── 個人枠売上・LP還元額(確定/見込み)・累計・支払予定
│
├── アクセスランキング (/dashboard/rankings)
│ └── チケット販売順位・デイリーランキング
│
└── 通知一覧 (/dashboard/notifications)
4-3. 協会管理画面遷移(/admin配下)
協会ダッシュボード (/admin)
│
├── 試合管理 (/admin/events)
│ ├── 試合作成 (/admin/events/new)
│ ├── 試合編集 (/admin/events/:id/edit)
│ │ ├── 選手アサイン(モーダル)
│ │ ├── ノルマ設定(モーダル)
│ │ └── 宣材写真アップロード(モーダル)
│ └── 試合販売状況 (/admin/events/:id/sales)
│
├── LP管理 (/admin/lps)
│ ├── LP生成 → 生成中ローディング → プレビュー(全画面モーダル)
│ ├── ビジュアルエディタ(全画面)
│ └── 公開管理(ステータス変更)
│
├── 選手管理 (/admin/players)
│ ├── 選手アカウント作成(モーダル)
│ ├── 選手詳細(スライドオーバー)
│ │ ├── [タブ] 販売状況チャート
│ │ ├── [タブ] ファン一覧
│ │ ├── [タブ] スポンサー一覧
│ │ └── [タブ] 還元履歴
│ └── アカウントロック解除
│
├── 還元管理 (/admin/rewards)
│ ├── 試合別LP売上一覧
│ ├── 選手別還元額・支払管理
│ └── CSV出力
│
├── スポンサー管理 (/admin/sponsors)
│ ├── スポンサー登録(モーダル)
│ ├── スポンサー詳細(スライドオーバー)
│ └── 公開ページ掲載承認
│
├── キックバック管理 (/admin/kickbacks)
│ ├── キックバック登録(モーダル)
│ ├── 支払管理
│ └── CSV出力
│
├── アクセスランキング (/admin/rankings)
│ ├── チケット販売ランキング
│ ├── 選手ページPVランキング
│ ├── デイリーランキング
│ └── LP経由販売ランキング
│
├── 操作ログ (/admin/audit-logs)
│
└── 通知一覧 (/admin/notifications)
4-4. concept管理画面遷移(/concept配下)
concept営業リード管理 (/concept)
├── リード一覧 (/concept/leads)
│ ├── ステータス更新(インラインまたはモーダル)
│ └── 協会への公開許可/取消
│
├── キックバック管理 (/concept/kickbacks)
│ └── キックバック登録・支払管理
│
├── 操作ログ (/concept/audit-logs)
│
└── 通知一覧 (/concept/notifications)
4-5. 全21画面一覧と遷移ルール
| # | 画面名 | パス | 主要遷移元 | 主要遷移先 |
|---|
| 1 | トップ(公開) | / | 外部・SNS | 2, 3, 4 |
| 2 | 試合LP(公開) | /events/:id | 1, SNS | チケットぴあ外部URL |
| 3 | 選手個別公開ページ | /fighters/:slug | 1, SNS, QR | チケットぴあ外部URL |
| 4 | ログイン | /login | 1 | 5, 6, 12, 19 |
| 5 | 初回パスワード変更 | /change-password | 4 | 6, 12, 19 |
| 6 | 選手ホーム | /dashboard | 4, 5 | 7-11, 20, 21 |
| 7 | ファンCRM | /dashboard/fans | 6 | 6 |
| 8 | QR・URL生成 | /dashboard/qr | 6 | SNS外部 |
| 9 | 手売り入力フォーム | /dashboard/manual-sales | 6 | 6 |
| 10 | 公開ページ編集 | /dashboard/profile | 6 | 3 |
| 11 | 収入サマリー | /dashboard/income | 6 | 6 |
| 12 | 協会ダッシュボード | /admin | 4, 5 | 13-18, 20, 21 |
| 13 | 試合管理 | /admin/events | 12 | 14 |
| 14 | LP生成・編集・公開 | /admin/lps | 12, 13 | 2 |
| 15 | 還元管理 | /admin/rewards | 12 | 15 |
| 16 | 選手管理 | /admin/players | 12 | 16 |
| 17 | スポンサー管理 | /admin/sponsors | 12 | 17 |
| 18 | キックバック管理 | /admin/kickbacks | 12 | 18 |
| 19 | concept営業リード管理 | /concept | 4, 5 | 19 |
| 20 | アクセスランキング | /dashboard/rankings or /admin/rankings | 6, 12 | — |
| 21 | 操作ログ | /admin/audit-logs or /concept/audit-logs | 12, 19 | — |
5. ビジネスロジック
5-1. LP自動生成フロー
┌──────────────────────────────────────────────────────────────────┐
│ LP自動生成フロー │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Step 1: 管理者が「LP生成」ボタンをクリック │
│ ───────────────────────────────────────── │
│ ● フロントエンドから POST /api/v1/lps/generate {event_id} │
│ ● バックエンドで非同期ジョブを作成し job_id を即座に返却 │
│ ● LPステータスを 'draft' で作成 │
│ │
│ Step 2: データ収集(バックエンド非同期処理) │
│ ───────────────────────────────────────── │
│ ● events テーブルから試合情報を取得 │
│ ● event_players + players から出場選手・宣材写真を取得 │
│ ● sponsors から協会承認済みスポンサーを取得 │
│ ● 席種・定価情報を取得 │
│ ● 協会枠refコードを自動生成 (sb_association_event{event_id}) │
│ │
│ Step 3: Claude API呼び出し(LP HTML生成) │
│ ───────────────────────────────────────── │
│ ● プロンプト構成: │
│ - システムプロンプト: LP HTMLテンプレート・デザインガイドライン │
│ - ユーザープロンプト: 試合情報・選手情報・席種・紹介テキスト │
│ ● Claude APIパラメータ: │
│ - model: claude-sonnet-4-20250514 │
│ - max_tokens: 8192 │
│ - temperature: 0.3(安定した出力のため低めに設定) │
│ ● 出力: 完全なHTML/CSS(Tailwind CDN使用) │
│ ● タイムアウト: 30秒 │
│ │
│ Step 4: ヒービジュアル画像合成(Canvas API + sharp) │
│ ───────────────────────────────────────── │
│ ● 入力: 対戦選手の宣材写真(Supabase Storageから取得) │
│ ● 処理: │
│ (1) sharp で宣材写真をリサイズ・クロップ │
│ (2) Canvas APIで vs構図レイアウトを生成 │
│ - 左: 選手A 写真 + 名前 + 所属 + 階級 │
│ - 中央: "VS" テキスト + 試合名 │
│ - 右: 選手B 写真 + 名前 + 所属 + 階級 │
│ (3) ダークグラデーション背景を合成 │
│ (4) 2サイズで出力: │
│ - OGP用: 1200×630px │
│ - ヒーロー用: 1920×1080px │
│ ● Supabase Storageにアップロード │
│ ● タイムアウト: 10秒 │
│ │
│ Step 5: LP HTMLにヒービジュアル・CTAを埋め込み │
│ ───────────────────────────────────────── │
│ ● Claude生成HTMLの画像プレースホルダーに実際のURLを挿入 │
│ ● 席種別チケット購入CTAボタンに協会枠ref付きURLを設定 │
│ ● OGPメタタグ(og:image, og:title, og:description)を設定 │
│ ● SNSシェアボタンのURLを設定 │
│ ● HTMLをlps.html_contentに保存 │
│ ● LPステータスを 'draft' のまま保持 │
│ │
│ Step 6: プレビュー確認(管理者) │
│ ───────────────────────────────────────── │
│ ● 管理者がプレビューモーダルでPC/スマホ表示を確認 │
│ ● ビジュアルエディタでテキスト・画像の編集が可能 │
│ ● 編集内容はeditor_data (JSONB)に保存 │
│ │
│ Step 7: 公開(管理者の明示的操作が必須) │
│ ───────────────────────────────────────── │
│ ● 「公開する」ボタン → 確認ダイアログ「本当に公開しますか?」 │
│ ● POST /api/v1/lps/:id/publish │
│ ● LPステータスを 'published' に変更 │
│ ● published_at, published_by を記録 │
│ ● 全出場選手にプッシュ通知「LPが公開されました!シェアしよう」 │
│ ● SNSシェアボタンを表示 │
│ │
└──────────────────────────────────────────────────────────────────┘
エラーハンドリング:
- Claude API タイムアウト(30秒超過)→ ジョブステータスを 'failed' に設定し管理者に通知
- 画像合成失敗 → デフォルトヒービジュアル(テキストのみ)で代替生成
- 宣材写真未登録 → 選手名テキストのプレースホルダーで生成
5-2. チケット購入紐づけフロー
┌──────────────────────────────────────────────────────────────────┐
│ チケット購入紐づけフロー │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 【パターンA: ぴあWebhook経由(選手個人枠)】 │
│ │
│ 1. ファンが選手QR/URL経由でぴあ購入ページにアクセス │
│ URL例: https://t.pia.jp/xxxx?ref=sb_player007_event001 │
│ │
│ 2. ファンがぴあで購入完了 │
│ │
│ 3. ぴあからWebhookコールバック │
│ POST /api/v1/tickets/webhook/pia │
│ Headers: X-Pia-Signature: <HMAC-SHA256署名> │
│ Body: {ref, buyer_name, seat_type, quantity, amount, ...} │
│ │
│ 4. バックエンド処理: │
│ (a) HMAC-SHA256署名を検証(不正なら即リジェクト + ログ記録) │
│ (b) refコードをパース → player_id + event_id を特定 │
│ ref形式: sb_{player_slug}_{event_short_id} │
│ (c) event_players テーブルで ref_code を照合 │
│ (d) tickets テーブルに販売実績を記録 │
│ route = 'player_pia' │
│ (e) fan テーブルの購入者を特定 or 新規作成 │
│ (buyer_nameで既存ファンを候補表示→選手が後で紐づけ) │
│ (f) 担当選手にSMS+プッシュ通知 │
│ 「[試合名]のチケットが[N]枚購入されました」 │
│ (g) Webhookペイロード全体をwebhook_payloadに保存(監査用) │
│ │
│ 【パターンB: ぴあWebhook経由(協会LP枠)】 │
│ │
│ 1. ファンがLP上のCTAボタンからぴあにアクセス │
│ URL例: https://t.pia.jp/xxxx?ref=sb_association_event001 │
│ │
│ 2. 同様のWebhookフロー │
│ ただし player_id = NULL, route = 'association_lp' │
│ │
│ 3. 協会管理者にプッシュ通知 │
│ 「[LP名]からチケットが[N]枚購入されました」 │
│ │
│ 【パターンC: 手売り(選手が直接入力)】 │
│ │
│ 1. 選手が手売り入力フォームで記録 │
│ POST /api/v1/tickets/manual │
│ {event_id, buyer_name, quantity, seat_type, │
│ payment_method, is_paid} │
│ │
│ 2. route = 'player_cash' or 'player_transfer' │
│ │
│ 【パターンD: 当日券(協会管理者が一括入力)】 │
│ │
│ 1. 試合終了後に協会管理者が一括入力 │
│ POST /api/v1/tickets/day-tickets │
│ [{event_id, quantity, seat_type}] │
│ │
│ 2. route = 'day_ticket', player_id = NULL │
│ │
└──────────────────────────────────────────────────────────────────┘
refコード命名規則:
- 選手個人枠:
sb_{player_slug}_{event_short_id} (例: sb_kasahara-naoki_ev001)
- 協会LP枠:
sb_association_{event_short_id} (例: sb_association_ev001)
5-3. 還元額計算ロジック
┌──────────────────────────────────────────────────────────────────┐
│ 還元額計算ロジック │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 【計算タイミング】 │
│ ● 手動: 協会管理者が「還元額確定」ボタンを押下 │
│ ● 自動: 試合開催日の翌日 00:00 (JST) にバッチジョブ実行 │
│ │
│ 【計算フロー】 │
│ │
│ 1. 対象LP経由販売データの集計 │
│ SELECT SUM(total_amount) as lp_sales │
│ FROM tickets │
│ WHERE event_id = :event_id │
│ AND route = 'association_lp' │
│ AND is_paid = true │
│ │
│ 2. 出場選手ごとの還元額計算 │
│ FOR EACH event_player IN event_players: │
│ reward_amount = lp_sales * (reward_rate / 100) │
│ │
│ ※ 還元率は試合×選手ごとに個別設定(event_players.reward_rate) │
│ ※ 全選手同一率もあれば個別設定もある │
│ │
│ 3. player_rewards テーブルに記録 │
│ INSERT INTO player_rewards │
│ (event_id, player_id, lp_sales_amount, │
│ reward_rate, reward_amount, status) │
│ VALUES (:event_id, :player_id, :lp_sales, │
│ :rate, :amount, 'confirmed') │
│ │
│ 4. 選手に通知 │
│ SMS + プッシュ: │
│ 「[試合名]のLP還元額が確定しました:¥{reward_amount}」 │
│ │
│ 【見込み還元額(リアルタイム)】 │
│ ● 試合開催前の見込み額はリアルタイムで計算 │
│ ● GET /players/me/income で返却 │
│ estimate = 現在のLP経由売上 × 還元率 │
│ ● statusは 'pending' のまま │
│ │
│ 【支払フロー】 │
│ ● 協会管理者が「支払済」ボタンをクリック │
│ ● POST /admin/rewards/:id/mark-paid {paid_date} │
│ ● status = 'paid' に変更 │
│ ● 選手に通知「[金額]円を振り込みました」 │
│ │
└──────────────────────────────────────────────────────────────────┘
計算例:
試合: SHOOT BOXING 2026 act.2
LP経由売上合計: 500,000円
選手A(還元率50%): 500,000 × 0.50 = 250,000円
選手B(還元率30%): 500,000 × 0.30 = 150,000円
選手C(還元率50%): 500,000 × 0.50 = 250,000円
5-4. ノルマ管理・達成率計算
┌──────────────────────────────────────────────────────────────────┐
│ ノルマ管理・達成率計算 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 【設定】 │
│ ● 協会管理者が設定(event_players.quota) │
│ ● 一律設定: 全選手に同一枚数を一括設定 │
│ ● 個別設定: 選手ごとに異なる枚数を設定 │
│ ● 選手は閲覧のみ(編集不可) │
│ │
│ 【達成率計算】 │
│ │
│ sold_count = SUM(quantity) FROM tickets │
│ WHERE event_id = :event_id │
│ AND player_id = :player_id │
│ AND route IN ('player_pia','player_eplus', │
│ 'player_cash','player_transfer') │
│ │
│ achievement_rate = (sold_count / quota) × 100 │
│ │
│ ※ 協会LP枠(association_lp)・当日券(day_ticket)は │
│ 個人ノルマの達成率には含めない │
│ │
│ 【色分けルール(UI表示)】 │
│ ● 80%以上: 緑 (#22C55E) — 達成目前 │
│ ● 50-80%: 黄 (#EAB308) — 注意 │
│ ● 50%未満: 赤 (#EF4444) — 要対応 │
│ │
│ 【ランキング表示ルール】 │
│ ● 全ロール: 順位のみ表示(他選手の具体的な販売枚数は非表示) │
│ ● 自分の販売枚数・達成率は表示 │
│ ● 協会管理者: 全選手の詳細数値を閲覧可能 │
│ │
└──────────────────────────────────────────────────────────────────┘
5-5. 掘り起こし判定ロジック
┌──────────────────────────────────────────────────────────────────┐
│ 掘り起こし判定ロジック │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 【判定条件】 │
│ 直近2試合に購入履歴がないファンを自動抽出 │
│ │
│ 【判定SQL(概念)】 │
│ │
│ -- 直近2試合のIDを取得 │
│ recent_events = SELECT id FROM events │
│ WHERE event_date <= NOW() │
│ ORDER BY event_date DESC │
│ LIMIT 2 │
│ │
│ -- 直近2試合で購入していないファンを抽出 │
│ dormant_fans = SELECT f.* FROM fans f │
│ WHERE f.player_id = :player_id │
│ AND f.id NOT IN ( │
│ SELECT DISTINCT t.fan_id FROM tickets t │
│ WHERE t.event_id IN (:recent_event_ids) │
│ AND t.fan_id IS NOT NULL │
│ ) │
│ │
│ 【自動更新タイミング】 │
│ ● 試合終了後のバッチジョブで全ファンの is_dormant フラグを更新 │
│ ● 新規購入時にも対象ファンの is_dormant を false に更新 │
│ │
│ 【掘り起こしアクション】 │
│ 1. 選手がファンCRM画面で掘り起こしリストを確認 │
│ 2. 対象ファンを選択 │
│ 3. 今回試合のref付きURL/QRをワンタップ生成 │
│ 4. LINE/X/コピーでSNSシェア │
│ │
└──────────────────────────────────────────────────────────────────┘
5-6. キックバック自動計算
┌──────────────────────────────────────────────────────────────────┐
│ キックバック自動計算 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 【計算式】 │
│ kickback_amount = contract_amount × (kickback_rate / 100) │
│ │
│ 【入力フロー】 │
│ 1. concept管理者がスポンサー経由でAGI案件を受注 │
│ 2. concept管理者 or 協会管理者がキックバック情報を登録 │
│ - sponsor_id: 紹介元スポンサー │
│ - player_id: 紹介した選手(任意) │
│ - contract_amount: AGI受注金額 │
│ - kickback_rate: キックバック割合(%) │
│ 3. kickback_amount はシステムが自動計算 │
│ │
│ 【支払管理】 │
│ ● payment_due_date: 支払予定日を手動設定 │
│ ● is_paid / paid_date: 支払実行後に手動マーク │
│ ● transfer_info_encrypted: 振込先情報(AES-256暗号化保存) │
│ │
│ 【試算参考値】 │
│ ● 1社受注 1,500万円 × 15% = 225万円 │
│ ● 3社受注 4,500万円 × 15% = 675万円 │
│ ● 5社受注 7,500万円 × 15% = 1,125万円 │
│ │
└──────────────────────────────────────────────────────────────────┘
5-7. アカウントロック・解除フロー
┌──────────────────────────────────────────────────────────────────┐
│ アカウントロック・解除フロー │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 【ロック条件】 │
│ パスワード or SMS認証コードを5回連続ミス │
│ │
│ 【ロックフロー】 │
│ 1. ログイン試行: │
│ (a) パスワード不一致 → failed_attempts +1 │
│ (b) SMS認証コード不一致 → failed_attempts +1 │
│ ※ パスワード正解→SMS失敗の場合もカウント │
│ │
│ 2. failed_attempts >= 5: │
│ (a) is_locked = true │
│ (b) locked_at = NOW() │
│ (c) audit_logs に 'account_locked' を記録 │
│ (d) ログイン画面に「アカウントがロックされました。 │
│ 協会管理者にお問い合わせください」を表示 │
│ │
│ 3. ロック中のログイン試行: │
│ 即座にエラー返却(パスワード検証自体を行わない) │
│ ※ タイミング攻撃防止のため一定の遅延を挿入 │
│ │
│ 【解除フロー】 │
│ 1. 協会管理者が選手管理画面で対象選手を選択 │
│ 2. 「ロック解除」ボタンをクリック │
│ 3. POST /api/v1/auth/unlock-account {user_type, user_id} │
│ 4. 処理: │
│ (a) is_locked = false │
│ (b) failed_attempts = 0 │
│ (c) locked_at = NULL │
│ (d) audit_logs に 'account_unlocked' を記録 │
│ │
│ 【レートリミット(追加防御)】 │
│ ● ログインAPI: IP単位で10回/分 │
│ ● Redis (Upstash) で管理 │
│ ● 超過時は 429 Too Many Requests を返却 │
│ │
│ 【パスワードリセット】 │
│ ● ロック中もパスワードリセットは可能 │
│ ● 「パスワードを忘れた」→ 登録電話番号にSMSでリセットコード │
│ ● リセット成功 → ロック解除 + failed_attempts = 0 │
│ ● 前回パスワードの再使用は不可 │
│ (previous_password_hash と照合) │
│ │
└──────────────────────────────────────────────────────────────────┘
5-8. 締切通知スケジュールロジック
┌──────────────────────────────────────────────────────────────────┐
│ 締切通知スケジュールロジック │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 【バッチジョブ】毎日 09:00 (JST) に実行 │
│ │
│ FOR EACH event IN upcoming_events: │
│ days_remaining = event.ticket_deadline_date - TODAY │
│ │
│ CASE days_remaining: │
│ 14日: │
│ → 出場全選手にプッシュ通知 │
│ 「[試合名]のチケット締切まで14日。達成率[X]%」 │
│ │
│ 7日 AND 選手の達成率 <= 50%: │
│ → 該当選手 + 協会管理者にSMS+プッシュ │
│ 「[選手名]のチケット残[N]枚。締切7日」 │
│ │
│ 3日: │
│ → 出場全選手にSMS+プッシュ │
│ 「[試合名]チケット締切まで3日!」 │
│ │
│ 1日: │
│ → 出場全選手 + 協会管理者にSMS+プッシュ │
│ 「明日が締切!現在達成率[X]%」 │
│ │
│ ※ 通知は notifications テーブルに記録後、 │
│ 非同期でSMS/Push送信 │
│ │
└──────────────────────────────────────────────────────────────────┘
6. 外部API連携
6-1. Twilio連携
Twilio Verify API(SMS二段階認証)
| 項目 | 仕様 |
|---|
| API | Twilio Verify Service |
| 用途 | ログイン時SMS認証・パスワードリセット |
| フロー | (1) Create Verification → (2) Check Verification |
| コード形式 | 6桁数字 |
| 有効期限 | 5分 |
| 再送制限 | 同一番号に60秒以内の再送不可 |
| 認証失敗 | 3回ミスでコード再送要求(5回連続ミスでアカウントロック) |
# 認証コード送信
from twilio.rest import Client
def send_verification(phone: str) -> str:
client = Client(TWILIO_SID, TWILIO_AUTH_TOKEN)
verification = client.verify.v2 \
.services(TWILIO_VERIFY_SERVICE_SID) \
.verifications \
.create(to=phone, channel='sms', locale='ja')
return verification.sid
# 認証コード検証
def check_verification(phone: str, code: str) -> bool:
client = Client(TWILIO_SID, TWILIO_AUTH_TOKEN)
check = client.verify.v2 \
.services(TWILIO_VERIFY_SERVICE_SID) \
.verification_checks \
.create(to=phone, code=code)
return check.status == 'approved'
Twilio Messaging API(通知SMS)
| 項目 | 仕様 |
|---|
| API | Twilio Messaging Service |
| 用途 | チケット購入通知・締切アラート・還元通知 |
| 送信元 | Twilio割り当て電話番号 |
| 文字数制限 | 全角70文字以内(SMS分割防止) |
| 送信レートリミット | 1秒1通 |
| エラーハンドリング | 送信失敗時はnotifications.error_messageに記録し、15分後にリトライ(最大3回) |
def send_sms(to: str, body: str) -> str:
client = Client(TWILIO_SID, TWILIO_AUTH_TOKEN)
message = client.messages.create(
body=body,
messaging_service_sid=TWILIO_MESSAGING_SERVICE_SID,
to=to
)
return message.sid
6-2. チケットぴあ連携
refパラメータ付きURL生成
| 項目 | 仕様 |
|---|
| URL形式 | {pia_base_url}?ref={ref_code} |
| ref形式(選手枠) | sb_{player_slug}_{event_short_id} |
| ref形式(協会枠) | sb_association_{event_short_id} |
| QRコード | ref付きURLをqrcode.jsでPNG変換 |
| QRサイズ | 標準: 300x300px / Instagram用: 1080x1920px |
Webhook受信
| 項目 | 仕様 |
|---|
| エンドポイント | POST /api/v1/tickets/webhook/pia |
| 署名検証 | HMAC-SHA256 |
| 署名ヘッダー | X-Pia-Signature |
| 署名検証キー | 環境変数 PIA_WEBHOOK_SECRET |
| タイムアウト | 5秒以内にレスポンス返却 |
| 冪等性 | Webhookペイロードの一意IDで重複処理を防止 |
| リトライ | ぴあ側が5xx受信時に最大3回リトライ(指数バックオフ) |
import hmac
import hashlib
def verify_pia_webhook(payload: bytes, signature: str) -> bool:
expected = hmac.new(
PIA_WEBHOOK_SECRET.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
注記: Phase 1ではWebhookはモック実装。ぴあとの正式契約後に本番Webhookエンドポイントに切り替え。モック期間中は手動入力またはテスト用Webhookシミュレータで動作検証。
6-3. Claude API連携(LP自動生成)
| 項目 | 仕様 |
|---|
| API | Anthropic Messages API |
| モデル | claude-sonnet-4-20250514 |
| max_tokens | 8192 |
| temperature | 0.3 |
| タイムアウト | 30秒 |
| リトライ | APIエラー時2回リトライ(指数バックオフ) |
| レートリミット | 10回/分(LP生成頻度的に十分) |
プロンプト構成:
system_prompt = """
あなたはプロのWebデザイナーです。格闘技の試合告知LPのHTMLを生成してください。
【デザインルール】
- ダークテーマ(背景: #0F1729)
- Tailwind CSS CDNを使用
- モバイルファーストのレスポンシブデザイン
- ブランドレッド (#E63946) をCTAカラーに使用
- ヒービジュアル画像のプレースホルダー: {{HERO_IMAGE_URL}}
- チケット購入CTAボタンのURL: {{CTA_URL_SEAT_TYPE}}
- スポンサーロゴのプレースホルダー: {{SPONSOR_LOGOS}}
【必須セクション】
1. ヒービジュアル(対戦カード)
2. 試合タイトル・日時・会場
3. 試合紹介文
4. 出場選手一覧(プロフィールカード)
5. 席種・料金表
6. チケット購入CTAボタン(席種別)
7. SNSシェアボタン
8. スポンサー枠
完全なHTML文書を出力してください。
"""
user_prompt = f"""
【試合情報】
試合名: {event.name}
開催日: {event.event_date}
会場: {event.venue}
開場: {event.door_open_time} / 開始: {event.start_time}
【出場選手】
{players_info}
【席種・料金】
{seat_types_info}
【試合紹介文】
{event.description}
【スポンサー】
{sponsors_info}
"""
6-4. Supabase連携
Supabase Auth
| 項目 | 仕様 |
|---|
| 認証方式 | カスタム認証(ID+パスワード+SMS) |
| JWT発行 | Supabase Auth のカスタムクレーム付きJWT |
| トークン管理 | アクセストークン: 15分 / リフレッシュ: 7日 |
| Cookie | HttpOnly, Secure, SameSite=Strict |
| RLSとの連携 | JWTのuser_id + roleでRLSポリシーを評価 |
Supabase Storage
| バケット | 用途 | アクセス | ファイル制限 |
|---|
player-images | 選手プロフィール画像 | 本人のみアップロード・公開読み取り | JPG/PNG, 5MB |
promo-images | 宣材写真 | admin アップロード・公開読み取り | JPG/PNG, 20MB |
sponsor-logos | スポンサーロゴ | admin アップロード・公開読み取り | JPG/PNG/SVG, 5MB |
lp-assets | LP生成画像(ヒービジュアル等) | system アップロード・公開読み取り | JPG/PNG, 10MB |
qr-codes | 生成済みQRコード | 本人のみ読み取り | PNG, 1MB |
Supabase RLS(Row Level Security)ポリシー
-- players テーブル: 選手は自分のデータのみ読み書き可能
CREATE POLICY "players_select_own" ON players
FOR SELECT USING (
auth.jwt()->>'role' = 'admin' OR
auth.jwt()->>'role' = 'staff' OR
id = auth.uid()
);
CREATE POLICY "players_update_own" ON players
FOR UPDATE USING (id = auth.uid())
WITH CHECK (id = auth.uid());
-- fans テーブル: 選手は自分のファンのみアクセス
CREATE POLICY "fans_select_own" ON fans
FOR SELECT USING (
auth.jwt()->>'role' = 'admin' OR
player_id = auth.uid()
);
-- tickets テーブル: 選手は自分のチケットのみ
CREATE POLICY "tickets_select_own" ON tickets
FOR SELECT USING (
auth.jwt()->>'role' IN ('admin', 'staff') OR
player_id = auth.uid()
);
-- sponsors テーブル: concept管理者は営業関連フィールドのみ
-- admins は全フィールド閲覧可能
-- staffは閲覧のみ
6-5. Redis (Upstash) 連携
| 用途 | キー形式 | TTL | 値 |
|---|
| ログインレートリミット | rate:login:{ip} | 60秒 | カウンタ(上限10) |
| APIレートリミット | rate:api:{user_id} | 60秒 | カウンタ(上限100) |
| SMS認証セッション | sms:session:{login_id} | 300秒(5分) | {user_type, user_id, verified: false} |
| LP生成ジョブ状態 | lp:job:{job_id} | 3600秒 | {status, progress, event_id} |
| Webhookリプレイ防止 | webhook:pia:{idempotency_key} | 86400秒 | processed |
7. セキュリティ実装方針
7-1. RBAC権限マトリクス
認証API
| エンドポイント | admin | player | staff | concept | 未認証 |
|---|
| POST /auth/login | - | - | - | - | OK |
| POST /auth/verify-sms | - | - | - | - | OK |
| POST /auth/refresh | OK | OK | OK | OK | - |
| POST /auth/logout | OK | OK | OK | OK | - |
| POST /auth/change-password | OK | OK | OK | OK | - |
| POST /auth/force-change-password | OK | OK | OK | OK | - |
| POST /auth/forgot-password | - | - | - | - | OK |
| POST /auth/reset-password | - | - | - | - | OK |
| POST /auth/unlock-account | OK | - | - | - | - |
試合管理API
| エンドポイント | admin | player | staff | concept | 未認証 |
|---|
| GET /events | OK | OK (自分の試合) | OK | - | - |
| GET /events/:id | OK | OK | OK | - | - |
| POST /events | OK | - | - | - | - |
| PUT /events/:id | OK | - | - | - | - |
| DELETE /events/:id | OK | - | - | - | - |
| POST /events/:id/players | OK | - | - | - | - |
| PUT /events/:id/players/:pid | OK | - | - | - | - |
| POST /events/:id/players/bulk-quota | OK | - | - | - | - |
| POST /events/:id/promo-images | OK | - | - | - | - |
| GET /events/:id/sales-summary | OK | - | OK | - | - |
| GET /events/public | - | - | - | - | OK |
| GET /events/:id/public | - | - | - | - | OK |
チケット販売API
| エンドポイント | admin | player | staff | concept | 未認証 |
|---|
| GET /tickets | OK | - | OK | - | - |
| GET /tickets/my | - | OK (自分のみ) | - | - | - |
| POST /tickets/manual | OK | OK (自分のみ) | - | - | - |
| PUT /tickets/:id | OK | OK (自分のみ) | - | - | - |
| DELETE /tickets/:id | OK | - | - | - | - |
| POST /tickets/day-tickets | OK | - | - | - | - |
| POST /tickets/webhook/pia | - | - | - | - | Webhook署名検証 |
| GET /tickets/my/stats | - | OK (自分のみ) | - | - | - |
| GET /tickets/export | OK | - | - | - | - |
選手API
| エンドポイント | admin | player | staff | concept | 未認証 |
|---|
| GET /players | OK | - | OK | - | - |
| GET /players/:id | OK | - | OK | - | - |
| POST /players | OK | - | - | - | - |
| PUT /players/:id | OK | - | - | - | - |
| DELETE /players/:id | OK | - | - | - | - |
| GET /players/me | - | OK | - | - | - |
| PUT /players/me | - | OK | - | - | - |
| POST /players/me/profile-image | - | OK | - | - | - |
| GET /players/me/dashboard | - | OK | - | - | - |
| GET /players/me/income | - | OK | - | - | - |
| GET /players/me/qr/:eid | - | OK | - | - | - |
| GET /players/public | - | - | - | - | OK |
| GET /players/public/:slug | - | - | - | - | OK |
LP生成API
| エンドポイント | admin | player | staff | concept | 未認証 |
|---|
| GET /lps | OK | - | OK | - | - |
| GET /lps/:id | OK | - | OK | - | - |
| POST /lps/generate | OK | - | - | - | - |
| GET /lps/generate/:jid/status | OK | - | - | - | - |
| PUT /lps/:id | OK | - | - | - | - |
| POST /lps/:id/publish | OK | - | - | - | - |
| POST /lps/:id/suspend | OK | - | - | - | - |
| POST /lps/:id/regenerate | OK | - | - | - | - |
| POST /lps/:id/hero-image | OK | - | - | - | - |
| GET /lps/public/:eid | - | - | - | - | OK |
| GET /lps/:id/preview | OK | - | OK | - | - |
ファンCRM API
| エンドポイント | admin | player | staff | concept | 未認証 |
|---|
| GET /fans | - | OK (自分のファンのみ) | - | - | - |
| GET /fans/:id | - | OK (自分のファンのみ) | - | - | - |
| POST /fans | - | OK | - | - | - |
| PUT /fans/:id | - | OK (自分のファンのみ) | - | - | - |
| DELETE /fans/:id | - | OK (自分のファンのみ) | - | - | - |
| GET /fans/dormant | - | OK | - | - | - |
| POST /fans/:id/share-url | - | OK (自分のファンのみ) | - | - | - |
| GET /fans/admin | OK | - | - | - | - |
| GET /fans/export | OK | - | - | - | - |
スポンサーAPI
| エンドポイント | admin | player | staff | concept | 未認証 |
|---|
| GET /sponsors | OK | - | OK | - | - |
| GET /sponsors/:id | OK | - | OK | OK | - |
| POST /sponsors | OK | - | - | - | - |
| PUT /sponsors/:id | OK | - | - | - | - |
| DELETE /sponsors/:id | OK | - | - | - | - |
| POST /sponsors/:id/logo | OK | - | - | - | - |
| POST /sponsors/:id/approve-display | OK | - | - | - | - |
| POST /sponsors/:id/revoke-display | OK | - | - | - | - |
| GET /sponsors/public/:slug | - | - | - | - | OK |
キックバックAPI
| エンドポイント | admin | player | staff | concept | 未認証 |
|---|
| GET /kickbacks | OK | - | - | OK | - |
| GET /kickbacks/:id | OK | - | - | OK | - |
| POST /kickbacks | OK | - | - | OK | - |
| PUT /kickbacks/:id | OK | - | - | OK | - |
| POST /kickbacks/:id/mark-paid | OK | - | - | OK | - |
| GET /kickbacks/summary | OK | - | - | OK | - |
| GET /kickbacks/export | OK | - | - | OK | - |
通知API
| エンドポイント | admin | player | staff | concept | 未認証 |
|---|
| GET /notifications | OK | OK (自分のみ) | OK (自分のみ) | OK (自分のみ) | - |
| PUT /notifications/:id/read | OK | OK (自分のみ) | OK (自分のみ) | OK (自分のみ) | - |
| PUT /notifications/read-all | OK | OK | OK | OK | - |
| GET /notifications/unread-count | OK | OK | OK | OK | - |
| POST /notifications/send | OK | - | - | - | - |
| POST /notifications/subscribe-push | OK | OK | OK | OK | - |
ランキングAPI
| エンドポイント | admin | player | staff | concept | 未認証 |
|---|
| GET /rankings/tickets | OK (全データ) | OK (順位のみ) | OK (順位のみ) | - | - |
| GET /rankings/daily | OK (全データ) | OK (順位のみ) | OK (順位のみ) | - | - |
| GET /rankings/page-views | OK | - | - | - | - |
| GET /rankings/lp-sales | OK | - | - | - | - |
管理者API
| エンドポイント | admin | player | staff | concept | 未認証 |
|---|
| GET /admin/dashboard | OK | - | OK (閲覧のみ) | - | - |
| GET /admin/rewards | OK | - | - | - | - |
| POST /admin/rewards/:id/confirm | OK | - | - | - | - |
| POST /admin/rewards/:id/mark-paid | OK | - | - | - | - |
| POST /admin/rewards/batch-confirm | OK | - | - | - | - |
| GET /admin/rewards/export | OK | - | - | - | - |
| GET /admin/audit-logs | OK | - | - | OK | - |
| POST /admin/accounts | OK | - | - | - | - |
| GET /admin/health | - | - | - | - | OK |
concept管理API
| エンドポイント | admin | player | staff | concept | 未認証 |
|---|
| GET /concept/leads | - | - | - | OK | - |
| PUT /concept/leads/:sid | - | - | - | OK | - |
| POST /concept/leads/:sid/publish | - | - | - | OK | - |
| POST /concept/leads/:sid/unpublish | - | - | - | OK | - |
| GET /concept/kickbacks | - | - | - | OK | - |
| GET /concept/dashboard | - | - | - | OK | - |
7-2. セキュリティヘッダー設定
# FastAPI middleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
# Security Headers
SECURITY_HEADERS = {
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Content-Security-Policy": (
"default-src 'self'; "
"script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com; "
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
"img-src 'self' data: https://*.supabase.co; "
"font-src 'self' https://fonts.gstatic.com; "
"connect-src 'self' https://*.supabase.co wss://*.supabase.co; "
"frame-ancestors 'none';"
),
}
7-3. CORS設定
from fastapi.middleware.cors import CORSMiddleware
ALLOWED_ORIGINS = [
"https://sb-ticket.com", # 本番
"https://sb-ticket-dev.aidreams-factory.com", # 開発
]
# ローカル開発時のみ追加
if ENV == "development":
ALLOWED_ORIGINS.append("http://localhost:3000")
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["*"],
)
7-4. 個人情報暗号化実装
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import base64
class FieldEncryptor:
"""個人情報フィールドのAES-256暗号化"""
def __init__(self, master_key: str, salt: bytes):
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=480000,
)
key = base64.urlsafe_b64encode(kdf.derive(master_key.encode()))
self.fernet = Fernet(key)
def encrypt(self, plaintext: str) -> bytes:
return self.fernet.encrypt(plaintext.encode())
def decrypt(self, ciphertext: bytes) -> str:
return self.fernet.decrypt(ciphertext).decode()
暗号化対象フィールド一覧:
| テーブル | フィールド |
|---|
| players | phone_encrypted, email_encrypted |
| admins | phone_encrypted |
| fans | phone_encrypted, email_encrypted |
| sponsors | contact_phone_encrypted, contact_email_encrypted |
| kickbacks | transfer_info_encrypted |
7-5. 入力バリデーション規則
| フィールド種別 | バリデーション規則 |
|---|
| パスワード | 8文字以上・大文字小文字英数字+記号を各1文字以上含む |
| ログインID | 英数字のみ・3-50文字 |
| 電話番号 | 日本の携帯番号形式(070/080/090始まり・ハイフンなし11桁) |
| メールアドレス | RFC 5322準拠 |
| URL | http/httpsスキーム・最大500文字 |
| 金額 | 正の整数・最大10桁 |
| 画像アップロード | MIMEタイプ(image/jpeg, image/png)・サイズ上限(フィールドごと)・拡張子検証 |
| テキストフィールド | XSSペイロード除去・HTMLタグストリップ(LP HTML以外) |
| JSONB | スキーマバリデーション実施 |
7-6. 操作ログ実装方針
# 全管理操作をデコレータで自動記録
from functools import wraps
def audit_log(action: str, resource_type: str):
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# Before
before_state = await get_resource_state(resource_type, kwargs.get('id'))
# Execute
result = await func(*args, **kwargs)
# After
after_state = await get_resource_state(resource_type, kwargs.get('id'))
# Log
await create_audit_log(
actor_type=current_user.role,
actor_id=current_user.id,
action=action,
resource_type=resource_type,
resource_id=kwargs.get('id'),
changes={"before": before_state, "after": after_state},
ip_address=request.client.host,
user_agent=request.headers.get('user-agent'),
)
return result
return wrapper
return decorator
ログに絶対に含めない情報:
- パスワード(ハッシュ含む)
- 暗号化前の個人情報(電話番号・メール・振込先)
- JWT トークン
- APIキー
- SMS認証コード
Generated by CCAGI SDK Phase 2 - 2026-03-16
Source: docs/requirements/requirements.md v2.0, non-functional.md v2.0, design-requirements.md v2.0