← 成果物一覧に戻る

機能仕様書

Version 1.0.0 | SB-Ticket(シュートボクシング)

機能仕様書(spec.md)

プロジェクト: シュートボクシング協会 統合PWAアプリ(SB-Ticket) バージョン: 2.0 フェーズ: Phase 2 - 設計 作成日: 2026-03-16 ソース: docs/requirements/requirements.md, non-functional.md, design-requirements.md


目次

  1. システム概要
  2. データモデル設計
  3. API設計
  4. 画面遷移図
  5. ビジネスロジック
  6. 外部API連携
  7. セキュリティ実装方針

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 + TypeScriptUI構築
UIライブラリshadcn/ui + Tailwind CSSコンポーネント・スタイリング
アニメーションframer-motionインタラクション・トランジション
アイコンLucide Icons (lucide-react)SVGアイコン
PWAService Worker + Web Push APIオフライン・プッシュ通知
フロントホスティングVercelCDN配信
バックエンドFastAPI (Python 3.12+)REST API
バックホスティングRenderAPIサーバー
データベースSupabase (PostgreSQL 15)データ永続化・RLS・Auth
ファイルストレージSupabase Storage画像・LP資材保存
キャッシュRedis (Upstash)レートリミット・セッション
SMSTwilio Verify API / Messaging API二段階認証・通知
AIClaude API (Anthropic)LP HTML自動生成
画像合成Canvas API + sharp (Node.js)対戦カードビジュアル
QRコードqrcode.jsQR生成・ダウンロード
暗号化AES-256 (Python cryptography)個人情報暗号化

1-3. ロール定義

ロールIDロール名対象者概要
admin協会管理者協会スタッフ1-3名全機能のフルアクセス
player選手登録選手(数十名規模)自分のデータのみ操作
staffスタッフ協会スタッフ閲覧のみ(編集不可)
conceptconcept管理者株式会社concept社内営業リード管理・キックバック管理

2. データモデル設計

2-1. ER概要

players ──< event_players >── events
   │                            │
   │                            ├── lps
   │                            │
   ├── fans ──< purchases ──────┘
   │
   ├── sponsors ──< kickbacks
   │
   └── player_rewards

notifications ──> players / admins
audit_logs (全テーブル操作記録)

2-2. テーブル定義

players(選手)

カラム名制約説明
idUUIDPK, DEFAULT gen_random_uuid()選手ID
login_idVARCHAR(50)UNIQUE, NOT NULLログインID(英数字・協会が発行)
password_hashVARCHAR(255)NOT NULLbcryptハッシュ(コストファクター12)
nameVARCHAR(100)NOT NULL氏名
name_kanaVARCHAR(100)フリガナ
slugVARCHAR(100)UNIQUE, NOT NULL公開ページURL用スラッグ
gymVARCHAR(100)所属ジム
weight_classVARCHAR(50)階級
profile_textTEXTプロフィール文
profile_image_urlVARCHAR(500)プロフィール画像URL(Supabase Storage)
phone_encryptedTEXT電話番号(AES-256暗号化)
email_encryptedTEXTメールアドレス(AES-256暗号化)
sns_linksJSONBDEFAULT '{}'SNSリンク {twitter, instagram, line, ...}
roleuser_roleDEFAULT 'player'ロール
is_lockedBOOLEANDEFAULT falseアカウントロック状態
failed_attemptsINTEGERDEFAULT 0連続失敗回数
locked_atTIMESTAMPTZロック日時
last_password_hashVARCHAR(255)前回パスワードハッシュ(再使用防止)
must_change_passwordBOOLEANDEFAULT true初回ログイン時パスワード変更
password_changed_atTIMESTAMPTZ最終パスワード変更日時
phone_verifiedBOOLEANDEFAULT false電話番号認証済フラグ
push_subscriptionJSONBWeb Push購読情報
created_atTIMESTAMPTZDEFAULT NOW()作成日時
updated_atTIMESTAMPTZDEFAULT NOW()更新日時
deleted_atTIMESTAMPTZ論理削除日時

インデックス: idx_players_login_id (login_id), idx_players_slug (slug)

admins(管理者 - 協会管理者・スタッフ・concept管理者)

カラム名制約説明
idUUIDPK, DEFAULT gen_random_uuid()管理者ID
login_idVARCHAR(50)UNIQUE, NOT NULLログインID
password_hashVARCHAR(255)NOT NULLbcryptハッシュ
nameVARCHAR(100)NOT NULL氏名
phone_encryptedTEXT電話番号(AES-256暗号化)
roleuser_roleNOT NULL, CHECK (role IN ('admin', 'staff', 'concept'))'admin' / 'staff' / 'concept'
is_lockedBOOLEANDEFAULT falseアカウントロック状態
failed_attemptsINTEGERDEFAULT 0連続失敗回数
locked_atTIMESTAMPTZロック日時
last_password_hashVARCHAR(255)前回パスワードハッシュ
must_change_passwordBOOLEANDEFAULT true初回ログイン時パスワード変更
phone_verifiedBOOLEANDEFAULT false電話番号認証済フラグ
push_subscriptionJSONBWeb Push購読情報
created_atTIMESTAMPTZDEFAULT NOW()
updated_atTIMESTAMPTZDEFAULT NOW()
deleted_atTIMESTAMPTZ

events(試合・イベント)

カラム名制約説明
idUUIDPK, DEFAULT gen_random_uuid()試合ID
nameVARCHAR(100)NOT NULL試合名
event_dateDATENOT NULL開催日
venueVARCHAR(100)NOT NULL会場名
door_open_timeTIME開場時間
start_timeTIMENOT NULL開始時間
ticket_sale_dateDATENOT NULLチケット発売日
ticket_deadline_dateDATENOT NULLチケット販売締切日
seat_typesJSONBNOT NULL席種・定価 [{name, price}]
sales_channelsTEXT[]NOT NULL販売経路 ['pia','eplus','cash']
pia_base_urlTEXTチケットぴあ基底URL
descriptionTEXT試合紹介テキスト
promo_imagesTEXT[]DEFAULT '{}'宣材写真URL(複数)
statusVARCHAR(20)DEFAULT 'upcoming''upcoming' / 'ongoing' / 'completed' / 'cancelled'
created_byUUIDFK → admins(id)作成者
created_atTIMESTAMPTZDEFAULT NOW()
updated_atTIMESTAMPTZDEFAULT NOW()
deleted_atTIMESTAMPTZ

インデックス: idx_events_date (event_date), idx_events_status (status)

event_players(試合×選手 中間テーブル)

カラム名制約説明
idUUIDPK, DEFAULT gen_random_uuid()
event_idUUIDFK → events(id), NOT NULL試合ID
player_idUUIDFK → players(id), NOT NULL選手ID
quotaINTEGERDEFAULT 0ノルマ枚数
promo_imagesTEXT[]宣材写真URL(複数)
ref_codeVARCHAR(100)UNIQUE, NOT NULLref付きURLコード (例: sb_player007_event001)
pia_urlVARCHAR(500)ぴあ ref付き完全URL
eplus_urlVARCHAR(500)イープラス URL
reward_rateDECIMAL(5,2)DEFAULT 0LP経由売上の還元率(%)
created_atTIMESTAMPTZDEFAULT NOW()

ユニーク制約: uq_event_player (event_id, player_id) インデックス: idx_ep_ref_code (ref_code)

tickets(チケット販売実績)

カラム名制約説明
idUUIDPK, DEFAULT gen_random_uuid()
event_idUUIDFK → events(id), NOT NULL試合ID
player_idUUIDFK → players(id)担当選手(NULL=協会枠)
fan_idUUIDFK → fans(id)購入ファン(特定可能な場合)
seat_typeVARCHAR(50)NOT NULL席種名
quantityINTEGERNOT NULL, CHECK > 0購入枚数
unit_priceINTEGERNOT NULL単価(円)
total_amountINTEGERNOT NULL合計金額
routeticket_routeNOT NULL販売経路(下記ENUM参照)
buyer_nameVARCHAR(100)購入者名
payment_methodpayment_method支払方法 ('cash'/'transfer'/'other')
is_paidBOOLEANDEFAULT false支払済フラグ
paid_atTIMESTAMPTZ支払日時
memoVARCHAR(200)備考
webhook_payloadJSONBぴあWebhook生データ(検証用)
purchased_atTIMESTAMPTZDEFAULT NOW()購入日時
created_atTIMESTAMPTZDEFAULT NOW()
updated_atTIMESTAMPTZDEFAULT NOW()

route ENUM値:

インデックス: 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)

カラム名制約説明
idUUIDPK, DEFAULT gen_random_uuid()ファンID
player_idUUIDFK → players(id), NOT NULL紐づく選手
nameVARCHAR(100)購入者名
phone_encryptedBYTEA電話番号(AES-256暗号化)
email_encryptedBYTEAメール(AES-256暗号化)
memoTEXT選手メモ欄
last_purchase_event_idUUIDFK → events(id)最終購入試合ID
last_purchase_atTIMESTAMPTZ最終購入日時
total_purchasesINTEGERDEFAULT 0累計購入枚数
is_dormantBOOLEANDEFAULT false掘り起こし対象フラグ
created_atTIMESTAMPTZDEFAULT NOW()
updated_atTIMESTAMPTZDEFAULT NOW()
deleted_atTIMESTAMPTZ

インデックス: idx_fans_player (player_id), idx_fans_dormant (is_dormant, player_id)

sponsors(スポンサー企業)

カラム名制約説明
idUUIDPK, DEFAULT gen_random_uuid()
player_idUUIDFK → players(id), NOT NULL紐づく選手
company_nameVARCHAR(200)NOT NULL企業名
industryVARCHAR(100)業種
contact_nameVARCHAR(100)NOT NULL担当者名
contact_phone_encryptedBYTEANOT NULL担当者電話(暗号化)
contact_email_encryptedBYTEA担当者メール(暗号化)
logo_urlVARCHAR(500)ロゴ画像URL
website_urlVARCHAR(500)企業サイトURL
contract_amountINTEGER契約金額(円)
contract_start_dateDATE契約開始日
contract_end_dateDATE契約終了日
concept_sales_statusconcept_statusDEFAULT 'untouched'concept営業ステータス
concept_memoTEXTconcept内部メモ
is_public_to_associationBOOLEANDEFAULT false協会への公開許可フラグ
is_approved_for_displayBOOLEANDEFAULT false公開ページ掲載承認フラグ
memoTEXT備考
created_atTIMESTAMPTZDEFAULT NOW()
updated_atTIMESTAMPTZDEFAULT NOW()
deleted_atTIMESTAMPTZ

concept_sales_status ENUM値: untouched / approached / negotiating / closed / passed

インデックス: idx_sponsors_player (player_id), idx_sponsors_concept_status (concept_sales_status)

kickbacks(キックバック)

カラム名制約説明
idUUIDPK, DEFAULT gen_random_uuid()
sponsor_idUUIDFK → sponsors(id), NOT NULL紹介元スポンサー
player_idUUIDFK → players(id)紹介した選手(任意)
contract_amountINTEGERNOT NULLconcept AGI受注金額(円)
kickback_rateDECIMAL(5,2)NOT NULLキックバック割合(%)
kickback_amountINTEGERGENERATED ALWAYS AS (FLOOR(contract_amount * kickback_rate / 100)) STORED自動計算: contract_amount * kickback_rate / 100
payment_due_dateDATE支払予定日
is_paidBOOLEANDEFAULT false支払済フラグ
paid_dateDATE実際の支払日
transfer_info_encryptedBYTEA振込先情報(暗号化)
memoTEXT備考
created_atTIMESTAMPTZDEFAULT NOW()
updated_atTIMESTAMPTZDEFAULT NOW()

インデックス: idx_kickbacks_sponsor (sponsor_id), idx_kickbacks_paid (is_paid)

lps(試合LP - ランディングページ)

カラム名制約説明
idUUIDPK, DEFAULT gen_random_uuid()
event_idUUIDFK → events(id), NOT NULL, UNIQUE試合ID(1試合1LP)
statuslp_statusDEFAULT 'draft''draft' / 'reviewing' / 'published' / 'unpublished'
html_contentTEXT生成済みLP HTML
hero_image_urlTEXTヒービジュアル画像URL
ogp_image_urlTEXTOGP画像URL (1200x630)
meta_titleVARCHAR(200)metaタイトル
meta_descriptionTEXTmetaディスクリプション
association_ref_codeVARCHAR(100)UNIQUE協会枠refコード (例: sb_association_event001)
association_pia_urlTEXT協会枠ぴあURL
editor_dataJSONBビジュアルエディタ保存データ
generated_by_modelVARCHAR(100)生成に使用したAIモデル名
published_atTIMESTAMPTZ公開日時
published_byUUIDFK → admins(id)公開許可した管理者
unpublished_atTIMESTAMPTZ非公開日時
created_atTIMESTAMPTZDEFAULT NOW()
updated_atTIMESTAMPTZDEFAULT NOW()

インデックス: idx_lps_event (event_id), idx_lps_status (status)

player_rewards(選手還元 - LP経由売上の還元管理)

カラム名制約説明
idUUIDPK, DEFAULT gen_random_uuid()
event_idUUIDFK → events(id), NOT NULL試合ID
player_idUUIDFK → players(id), NOT NULL選手ID
lp_sales_amountINTEGERDEFAULT 0LP経由売上金額
reward_rateDECIMAL(5,2)NOT NULL還元率(%)
reward_amountINTEGERGENERATED ALWAYS AS (FLOOR(lp_sales_amount * reward_rate / 100)) STORED還元額 = lp_sales_amount * reward_rate / 100
statusVARCHAR(20)DEFAULT 'pending''pending' / 'confirmed' / 'paid'
payment_due_dateDATE支払予定日
paid_dateDATE実際の支払日
confirmed_atTIMESTAMPTZ確定日時
confirmed_byUUIDFK → admins(id)確定した管理者
memoTEXT備考
created_atTIMESTAMPTZDEFAULT NOW()
updated_atTIMESTAMPTZDEFAULT NOW()

ユニーク制約: uq_reward_event_player (event_id, player_id)

notifications(通知)

カラム名制約説明
idUUIDPK, DEFAULT gen_random_uuid()
recipient_typeuser_roleNOT NULL'player' / 'admin'
recipient_idUUIDNOT NULL送信先のplayer_id or admin_id
typenotification_typeNOT NULL通知種別(下記参照)
methodnotification_methodNOT NULL通知方法: 'sms' / 'push' / 'both'
titleVARCHAR(200)NOT NULL通知タイトル
bodyTEXTNOT NULL通知本文
related_event_idUUID関連試合ID
related_player_idUUID関連選手ID
is_readBOOLEANDEFAULT false既読フラグ
is_sentBOOLEANDEFAULT false送信済フラグ
sent_atTIMESTAMPTZ送信日時
sms_sidVARCHAR(100)Twilio SMS SID
error_messageTEXT送信エラー内容
created_atTIMESTAMPTZDEFAULT NOW()

通知type一覧(notification_type ENUM):

通知method一覧(notification_method ENUM):

インデックス: idx_notifications_recipient (recipient_type, recipient_id), idx_notifications_read (is_read)

audit_logs(操作ログ)

カラム名制約説明
idUUIDPK, DEFAULT gen_random_uuid()
actor_typeVARCHAR(10)NOT NULL'player' / 'admin' / 'system'
actor_idUUID操作者ID
actionVARCHAR(50)NOT NULL操作種別
resource_typeVARCHAR(50)NOT NULL対象テーブル名
resource_idUUID対象レコードID
changesJSONB変更内容 {before, after}
ip_addressINETIPアドレス
user_agentTEXTユーザーエージェント
created_atTIMESTAMPTZDEFAULT NOW()

action一覧(主要):

インデックス: idx_audit_actor (actor_type, actor_id), idx_audit_resource (resource_type, resource_id), idx_audit_created (created_at)

page_views(ページ閲覧数 - ランキング用)

カラム名制約説明
idUUIDPK, DEFAULT gen_random_uuid()
page_typeVARCHAR(20)NOT NULL'player_page' / 'lp'
player_idUUIDFK → players(id)選手ID(選手ページの場合)
event_idUUIDFK → events(id)試合ID(LPの場合)
viewed_atDATENOT NULL閲覧日
countINTEGERDEFAULT 1日次集計カウント
created_atTIMESTAMPTZDEFAULT 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認証コード - 一時テーブル)

カラム名制約説明
idUUIDPK
user_typeVARCHAR(10)NOT NULL'player' / 'admin'
user_idUUIDNOT NULL
code_hashVARCHAR(255)NOT NULL6桁コードのハッシュ
attemptsINTEGERDEFAULT 0入力試行回数
expires_atTIMESTAMPTZNOT NULL有効期限(5分後)
created_atTIMESTAMPTZDEFAULT 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

レートリミット:

3-1. 認証API(/api/v1/auth)

メソッドパス説明認可リクエスト概要レスポンス概要
POST/auth/loginログイン(Step1: ID+パスワード)なし{login_id, password}{requires_sms: true, user_type}
POST/auth/verify-smsSMS認証コード検証(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宣材写真アップロードadminmultipart/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プロフィール画像アップロードplayermultipart/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_idQRコード生成player?size=standard&format=pngPNG画像
GET/players/me/qr/:event_id/instagramInstagram用QR画像生成playerPNG画像 (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/lpsLP一覧admin, staff?status=&event_id=[{lp_summary}]
GET/lps/:idLP詳細admin, staff{lp, event, editor_data}
POST/lps/generateLP自動生成admin{event_id}{lp, job_id} ※非同期
GET/lps/generate/:job_id/statusLP生成ジョブ状態確認admin{status: 'processing'/'completed'/'failed', progress}
PUT/lps/:idLP内容更新(エディタ保存)admin{editor_data, html_content}{lp}
POST/lps/:id/publishLP公開admin{lp, published_url}
POST/lps/:id/suspendLP公開停止admin{lp}
POST/lps/:id/regenerateLP再生成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/previewLPプレビュー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ロゴ画像アップロードadminmultipart/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/exportCSV出力admin, conceptCSV 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-pushWeb 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-salesLP経由販売ランキング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/dashboardconceptダッシュボード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トップ(公開)/外部・SNS2, 3, 4
2試合LP(公開)/events/:id1, SNSチケットぴあ外部URL
3選手個別公開ページ/fighters/:slug1, SNS, QRチケットぴあ外部URL
4ログイン/login15, 6, 12, 19
5初回パスワード変更/change-password46, 12, 19
6選手ホーム/dashboard4, 57-11, 20, 21
7ファンCRM/dashboard/fans66
8QR・URL生成/dashboard/qr6SNS外部
9手売り入力フォーム/dashboard/manual-sales66
10公開ページ編集/dashboard/profile63
11収入サマリー/dashboard/income66
12協会ダッシュボード/admin4, 513-18, 20, 21
13試合管理/admin/events1214
14LP生成・編集・公開/admin/lps12, 132
15還元管理/admin/rewards1215
16選手管理/admin/players1216
17スポンサー管理/admin/sponsors1217
18キックバック管理/admin/kickbacks1218
19concept営業リード管理/concept4, 519
20アクセスランキング/dashboard/rankings or /admin/rankings6, 12
21操作ログ/admin/audit-logs or /concept/audit-logs12, 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シェアボタンを表示                                          │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

エラーハンドリング:

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コード命名規則:

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二段階認証)

項目仕様
APITwilio 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)

項目仕様
APITwilio 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自動生成)

項目仕様
APIAnthropic Messages API
モデルclaude-sonnet-4-20250514
max_tokens8192
temperature0.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日
CookieHttpOnly, 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-assetsLP生成画像(ヒービジュアル等)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

エンドポイントadminplayerstaffconcept未認証
POST /auth/login----OK
POST /auth/verify-sms----OK
POST /auth/refreshOKOKOKOK-
POST /auth/logoutOKOKOKOK-
POST /auth/change-passwordOKOKOKOK-
POST /auth/force-change-passwordOKOKOKOK-
POST /auth/forgot-password----OK
POST /auth/reset-password----OK
POST /auth/unlock-accountOK----

試合管理API

エンドポイントadminplayerstaffconcept未認証
GET /eventsOKOK (自分の試合)OK--
GET /events/:idOKOKOK--
POST /eventsOK----
PUT /events/:idOK----
DELETE /events/:idOK----
POST /events/:id/playersOK----
PUT /events/:id/players/:pidOK----
POST /events/:id/players/bulk-quotaOK----
POST /events/:id/promo-imagesOK----
GET /events/:id/sales-summaryOK-OK--
GET /events/public----OK
GET /events/:id/public----OK

チケット販売API

エンドポイントadminplayerstaffconcept未認証
GET /ticketsOK-OK--
GET /tickets/my-OK (自分のみ)---
POST /tickets/manualOKOK (自分のみ)---
PUT /tickets/:idOKOK (自分のみ)---
DELETE /tickets/:idOK----
POST /tickets/day-ticketsOK----
POST /tickets/webhook/pia----Webhook署名検証
GET /tickets/my/stats-OK (自分のみ)---
GET /tickets/exportOK----

選手API

エンドポイントadminplayerstaffconcept未認証
GET /playersOK-OK--
GET /players/:idOK-OK--
POST /playersOK----
PUT /players/:idOK----
DELETE /players/:idOK----
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

エンドポイントadminplayerstaffconcept未認証
GET /lpsOK-OK--
GET /lps/:idOK-OK--
POST /lps/generateOK----
GET /lps/generate/:jid/statusOK----
PUT /lps/:idOK----
POST /lps/:id/publishOK----
POST /lps/:id/suspendOK----
POST /lps/:id/regenerateOK----
POST /lps/:id/hero-imageOK----
GET /lps/public/:eid----OK
GET /lps/:id/previewOK-OK--

ファンCRM API

エンドポイントadminplayerstaffconcept未認証
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/adminOK----
GET /fans/exportOK----

スポンサーAPI

エンドポイントadminplayerstaffconcept未認証
GET /sponsorsOK-OK--
GET /sponsors/:idOK-OKOK-
POST /sponsorsOK----
PUT /sponsors/:idOK----
DELETE /sponsors/:idOK----
POST /sponsors/:id/logoOK----
POST /sponsors/:id/approve-displayOK----
POST /sponsors/:id/revoke-displayOK----
GET /sponsors/public/:slug----OK

キックバックAPI

エンドポイントadminplayerstaffconcept未認証
GET /kickbacksOK--OK-
GET /kickbacks/:idOK--OK-
POST /kickbacksOK--OK-
PUT /kickbacks/:idOK--OK-
POST /kickbacks/:id/mark-paidOK--OK-
GET /kickbacks/summaryOK--OK-
GET /kickbacks/exportOK--OK-

通知API

エンドポイントadminplayerstaffconcept未認証
GET /notificationsOKOK (自分のみ)OK (自分のみ)OK (自分のみ)-
PUT /notifications/:id/readOKOK (自分のみ)OK (自分のみ)OK (自分のみ)-
PUT /notifications/read-allOKOKOKOK-
GET /notifications/unread-countOKOKOKOK-
POST /notifications/sendOK----
POST /notifications/subscribe-pushOKOKOKOK-

ランキングAPI

エンドポイントadminplayerstaffconcept未認証
GET /rankings/ticketsOK (全データ)OK (順位のみ)OK (順位のみ)--
GET /rankings/dailyOK (全データ)OK (順位のみ)OK (順位のみ)--
GET /rankings/page-viewsOK----
GET /rankings/lp-salesOK----

管理者API

エンドポイントadminplayerstaffconcept未認証
GET /admin/dashboardOK-OK (閲覧のみ)--
GET /admin/rewardsOK----
POST /admin/rewards/:id/confirmOK----
POST /admin/rewards/:id/mark-paidOK----
POST /admin/rewards/batch-confirmOK----
GET /admin/rewards/exportOK----
GET /admin/audit-logsOK--OK-
POST /admin/accountsOK----
GET /admin/health----OK

concept管理API

エンドポイントadminplayerstaffconcept未認証
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()

暗号化対象フィールド一覧:

テーブルフィールド
playersphone_encrypted, email_encrypted
adminsphone_encrypted
fansphone_encrypted, email_encrypted
sponsorscontact_phone_encrypted, contact_email_encrypted
kickbackstransfer_info_encrypted

7-5. 入力バリデーション規則

フィールド種別バリデーション規則
パスワード8文字以上・大文字小文字英数字+記号を各1文字以上含む
ログインID英数字のみ・3-50文字
電話番号日本の携帯番号形式(070/080/090始まり・ハイフンなし11桁)
メールアドレスRFC 5322準拠
URLhttp/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

ログに絶対に含めない情報:


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