家族共有仕様 (FR-011)
| 項目 | 内容 |
|---|---|
| 対象FR | FR-011(家族共有) |
| 優先度 | 中 |
| ステータス | 詳細化済み |
| フェーズ | Phase 4 |
概要
ペットオーナーは家族メンバーを招待し、ペット単位でデータを共有できる。招待はメールアドレス指定または招待コード共有の2方式を提供する。共有メンバーにはリソース×CRUD単位の権限を付与でき、UX向上のためプリセット(閲覧のみ・編集可能・フルアクセス)を用意する。共有メンバーへの通知設定もメンバーごとにカスタマイズ可能。
前提条件(依存FR)
| 依存FR | 必要な機能 | 用途 |
|---|---|---|
| FR-001/002(認証) | ユーザーアカウント | 招待先ユーザーの識別 |
| FR-003(ペット管理) | ペット登録 | 共有対象のペット |
| FR-010(通知) | 通知設定 | 共有メンバーへの通知配信 |
招待方式
メール招待
- オーナーが共有先のメールアドレスと権限プリセットを指定して招待を作成
- システムが招待メールを送信(招待リンク付き)
- 受信者がリンクをクリックして承認
- 未登録ユーザーの場合、アカウント登録後に自動で共有が有効化される
招待コード
- オーナーが権限プリセットを指定して招待コードを生成
- オーナーが任意の方法(LINE、口頭等)でコードを共有
- 受信者がアプリ内でコードを入力して承認
招待仕様
| 項目 | 値 |
|---|---|
| コード形式 | 英数字8文字(nanoid生成) |
| 有効期限 | 7日(メール・コード共通) |
| 最大利用回数 | メール招待: 1回、コード招待: 1回 |
| 1ペットあたりの共有上限 | 10人 |
| 1ペットあたりの有効な招待上限 | 20件 |
権限モデル
リソース×CRUD
共有メンバーの権限はリソースごとにCRUD操作を制御する。
| リソース | キー | 対象データ |
|---|---|---|
| ペットプロフィール | pet | 名前・品種・性別等の基本情報 |
| 体重記録 | weight | 体重記録 |
| 食事記録 | meal | 食事記録 |
| 通院履歴 | vetVisit | 通院記録 |
| ワクチン記録 | vaccination | ワクチン接種記録 |
| 体調ログ | healthLog | 症状・体調記録 |
| 投薬管理 | medication | 投薬スケジュール・投薬ログ |
操作: create / read / update / delete
- ペットプロフィールの
createとdeleteは常にオーナー専用(共有メンバーには付与不可) - 各リソースの操作を個別に ON/OFF できる
権限プリセット
| プリセット | キー | 説明 |
|---|---|---|
| 閲覧のみ | viewer | 全リソース read のみ |
| 編集可能 | editor | 全リソース create/read/update。ペットは read/update |
| フルアクセス | full | 全リソース create/read/update/delete。ペットは read/update |
プリセット適用後にリソース単位でカスタマイズも可能(プリセットはあくまで初期値)。
プリセット詳細
viewer:
{ "pet": ["read"], "weight": ["read"], "meal": ["read"], "vetVisit": ["read"], "vaccination": ["read"], "healthLog": ["read"], "medication": ["read"]}editor:
{ "pet": ["read", "update"], "weight": ["create", "read", "update"], "meal": ["create", "read", "update"], "vetVisit": ["create", "read", "update"], "vaccination": ["create", "read", "update"], "healthLog": ["create", "read", "update"], "medication": ["create", "read", "update"]}full:
{ "pet": ["read", "update"], "weight": ["create", "read", "update", "delete"], "meal": ["create", "read", "update", "delete"], "vetVisit": ["create", "read", "update", "delete"], "vaccination": ["create", "read", "update", "delete"], "healthLog": ["create", "read", "update", "delete"], "medication": ["create", "read", "update", "delete"]}通知設定(共有メンバー向け)
共有メンバーにもペット×通知タイプ単位で通知設定を付与する。既存の notification_settings テーブルを共用する(共有成立時にデフォルト設定を自動作成)。
通知プリセット
| プリセット | キー | 説明 |
|---|---|---|
| すべて受信 | all | 全通知タイプ有効、プッシュ ON |
| 重要のみ | important_only | ワクチンのみ有効、プッシュ ON |
| 受信しない | none | 全通知タイプ無効 |
招待時にオーナーが通知プリセットを選択する。共有メンバーは承認後に自分の通知設定を自由に変更できる。
APIエンドポイント
全エンドポイントで認証必須(__Host-session Cookie)。
招待管理
| メソッド | パス | 概要 |
|---|---|---|
| POST | /api/pets/:petId/shares/invitations | 招待作成 |
| GET | /api/pets/:petId/shares/invitations | 招待一覧(オーナー用) |
| DELETE | /api/pets/:petId/shares/invitations/:invitationId | 招待取消 |
| POST | /api/shares/accept | 招待承認(メールリンク経由) |
| POST | /api/shares/accept-code | 招待承認(コード入力) |
共有管理
| メソッド | パス | 概要 |
|---|---|---|
| GET | /api/pets/:petId/shares | 共有メンバー一覧 |
| PATCH | /api/pets/:petId/shares/:shareId | 権限更新 |
| DELETE | /api/pets/:petId/shares/:shareId | 共有解除(オーナーが解除) |
| DELETE | /api/shares/:shareId | 共有離脱(メンバーが自主離脱) |
| GET | /api/shared-pets | 自分に共有されているペット一覧 |
プリセット
| メソッド | パス | 概要 |
|---|---|---|
| GET | /api/share-presets/permissions | 権限プリセット一覧 |
| GET | /api/share-presets/notifications | 通知プリセット一覧 |
POST /api/pets/:petId/shares/invitations — 招待作成
// リクエスト(メール招待){ "type": "email", "email": "family@example.com", "permissionPreset": "editor", "notificationPreset": "all"}// リクエスト(コード招待){ "type": "code", "permissionPreset": "viewer", "notificationPreset": "important_only"}| フィールド | 型 | 必須 | バリデーション |
|---|---|---|---|
| type | varchar | ○ | email / code |
| varchar | △ | type=email 時必須。有効なメールアドレス形式 | |
| permissionPreset | varchar | ○ | viewer / editor / full |
| notificationPreset | varchar | - | all / important_only / none(default: all) |
- 成功時:
201 Created - 自分自身のメールアドレス:
422 Unprocessable Entity - 既に共有済みのユーザー:
409 Conflict - 共有上限(10人)超過:
409 Conflict - 有効な招待上限(20件)超過:
409 Conflict - ペットが存在しない / 自分のペットでない:
404 Not Found
// 成功レスポンス(メール招待){ "data": { "id": 1, "type": "email", "email": "family@example.com", "permissionPreset": "editor", "status": "pending", "expiresAt": "2026-03-01T10:00:00Z", "createdAt": "2026-02-22T10:00:00Z" }}// 成功レスポンス(コード招待){ "data": { "id": 2, "type": "code", "code": "Ab3xK9mQ", "permissionPreset": "viewer", "status": "pending", "expiresAt": "2026-03-01T10:00:00Z", "createdAt": "2026-02-22T10:00:00Z" }}GET /api/pets/:petId/shares/invitations — 招待一覧
オーナーが自分のペットに対する招待一覧を取得する。ステータスでフィルタ可能。
| パラメータ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
| status | varchar | - | - | pending / accepted / expired / revoked |
- 成功時:
200 OK
DELETE /api/pets/:petId/shares/invitations/:invitationId — 招待取消
- 成功時:
204 No Content pending以外のステータス:409 Conflict
POST /api/shares/accept — 招待承認(メールリンク経由)
{ "token": "invitation-token-from-email-link"}- 成功時:
200 OK、共有情報を返す - トークン無効 / 期限切れ:
422 Unprocessable Entity - 未登録ユーザー:
401 Unauthorized(登録を促すメッセージを返す)
POST /api/shares/accept-code — 招待承認(コード入力)
{ "code": "Ab3xK9mQ"}- 成功時:
200 OK、共有情報を返す - コード無効 / 期限切れ:
422 Unprocessable Entity
// 承認成功レスポンス{ "data": { "id": 1, "petId": 1, "petName": "そうにゃ", "ownerName": "田中太郎", "permissions": { "pet": ["read", "update"], "weight": ["create", "read", "update"], "meal": ["create", "read", "update"], "vetVisit": ["create", "read", "update"], "vaccination": ["create", "read", "update"], "healthLog": ["create", "read", "update"], "medication": ["create", "read", "update"] }, "createdAt": "2026-02-22T10:00:00Z" }}GET /api/pets/:petId/shares — 共有メンバー一覧
{ "data": [ { "id": 1, "userId": 5, "email": "family@example.com", "permissions": { "pet": ["read", "update"], "weight": ["create", "read", "update"], "meal": ["create", "read", "update"], "vetVisit": ["create", "read", "update"], "vaccination": ["create", "read", "update"], "healthLog": ["create", "read", "update"], "medication": ["create", "read", "update"] }, "createdAt": "2026-02-22T10:00:00Z" } ]}PATCH /api/pets/:petId/shares/:shareId — 権限更新
// リクエスト(プリセットで一括変更){ "permissionPreset": "full"}// リクエスト(リソース単位でカスタマイズ){ "permissions": { "weight": ["create", "read", "update", "delete"], "meal": ["read"] }}permissionPresetとpermissionsは排他(両方指定時は422)permissionsは指定したリソースのみ上書き(未指定リソースは変更なし)- 成功時:
200 OK、更新後の共有情報を返す
DELETE /api/pets/:petId/shares/:shareId — 共有解除
オーナーが共有メンバーを解除する。
- 成功時:
204 No Content - 該当共有の通知設定も削除される
DELETE /api/shares/:shareId — 共有離脱
共有メンバーが自分から共有を離脱する。
- 成功時:
204 No Content
GET /api/shared-pets — 共有されているペット一覧
自分に共有されているペットの一覧を返す。
{ "data": [ { "shareId": 1, "pet": { "id": 1, "name": "そうにゃ", "breedId": 3, "sex": "female", "photoUrl": null }, "owner": { "id": 2, "email": "owner@example.com" }, "permissions": { "..." : "..." }, "createdAt": "2026-02-22T10:00:00Z" } ]}GET /api/share-presets/permissions — 権限プリセット一覧
{ "data": [ { "key": "viewer", "label": "閲覧のみ", "description": "すべてのデータを閲覧できます", "permissions": { "..." : "..." } }, { "key": "editor", "label": "編集可能", "description": "データの追加・編集ができます", "permissions": { "..." : "..." } }, { "key": "full", "label": "フルアクセス", "description": "削除を含むすべての操作ができます", "permissions": { "..." : "..." } } ]}GET /api/share-presets/notifications — 通知プリセット一覧
{ "data": [ { "key": "all", "label": "すべて受信", "description": "すべての通知をプッシュで受信します" }, { "key": "important_only", "label": "重要のみ", "description": "ワクチンのリマインドのみ受信します" }, { "key": "none", "label": "受信しない", "description": "通知を受信しません" } ]}認可ルール
- 全エンドポイントで認証必須
- 招待の作成・一覧・取消: ペットオーナーのみ
- 共有メンバー一覧: ペットオーナーのみ
- 権限更新・共有解除: ペットオーナーのみ
- 共有離脱: 共有メンバー自身のみ
- 共有ペットのデータアクセス:
pet_shares.permissionsに基づいて制御 - 権限がない操作を試みた場合:
403 Forbidden - 他ユーザーのペットID / 共有IDを指定した場合:
404 Not Found
既存エンドポイントへの影響
共有メンバーが既存のペット関連エンドポイント(体重記録、食事記録等)にアクセスする場合:
pet_sharesテーブルで該当ペットへの共有が存在するか確認- 該当リソース×操作の権限があるか
permissionsJSONを照合 - 権限がある場合のみ処理を実行、ない場合は
403 Forbidden
DBテーブル
pet_shares
| カラム | 型 | 備考 |
|---|---|---|
| id | serial | 主キー |
| pet_id | integer | FK → pets.id |
| user_id | integer | FK → users.id(共有先ユーザー) |
| permissions | jsonb | リソース×CRUD権限 |
| invited_by | integer | FK → users.id(招待者) |
| created_at | timestamptz | 作成日時 |
| updated_at | timestamptz | 更新日時 |
(pet_id, user_id)に UNIQUE 制約- ペットオーナー自身を共有先にすることはできない
share_invitations
| カラム | 型 | 備考 |
|---|---|---|
| id | serial | 主キー |
| pet_id | integer | FK → pets.id |
| inviter_id | integer | FK → users.id |
| invite_type | varchar | email / code |
| varchar | 招待先メールアドレス(nullable、email タイプ時のみ) | |
| code | varchar | 招待コード(nullable、code タイプ時のみ、UNIQUE) |
| token | varchar | メール招待リンク用トークン(nullable、email タイプ時のみ) |
| permissions | jsonb | 付与する権限(プリセット展開後の実体) |
| notification_preset | varchar | 通知プリセットキー |
| status | varchar | pending / accepted / expired / revoked |
| expires_at | timestamptz | 有効期限 |
| created_at | timestamptz | 作成日時 |
- 期限切れの
pending招待は1日1回のバッチでexpiredに更新
エッジケース
- 未登録ユーザーへのメール招待 → 招待メールは送信する。未登録ユーザーが新規登録後に招待を承認すると共有が成立する
- 自分自身への招待 →
422 Unprocessable Entity - 既に共有済みのユーザーへの再招待 →
409 Conflict - 招待コードを複数人に共有した場合 → 最初の1人のみ承認可能(1回限り)
- オーナーがペットを論理削除した場合 → 既存の共有は保持するが、共有メンバーからのアクセスは
404を返す - 共有メンバーのアカウントが停止された場合 → 共有は保持するが、該当ユーザーはログインできないためアクセス不可
- 権限変更後のセッション → 即座に反映(リクエストごとに権限を確認)
- 共有解除後 → 該当ペットの通知設定も削除される
- オーナーがアカウント削除された場合 → ペットの共有も全て無効化される
拡張予定(現時点ではスコープ外)
- オーナー権限の移譲(オーナー変更)
- 共有メンバーの招待権限(メンバーが別のメンバーを招待)
- 共有グループ(家族単位での一括管理)
- 共有メンバーの活動ログ(誰がいつ何を変更したか)
- 共有ペットへのコメント・メッセージ機能
検証方法
- メール招待でペットが共有され、共有メンバーがデータを閲覧できること
- コード招待でペットが共有されること
- 権限プリセット(viewer/editor/full)が正しく適用されること
- 権限のないリソース・操作に
403が返ること - 権限のカスタマイズ(プリセット後の個別変更)が反映されること
- 共有上限(10人)を超えた場合にエラーになること
- 有効期限切れの招待が承認できないこと
- オーナーが共有を解除でき、メンバーのアクセスが不可になること
- メンバーが自主離脱でき、ペットが一覧から消えること
- 未登録ユーザーへの招待が新規登録後に承認できること
- 共有メンバーに通知設定が自動作成されること
- 共有解除後に通知設定が削除されること
- 自分自身への招待が拒否されること
- 他ユーザーのペットへの招待操作が
404を返すこと