---
title: "メンバー データ設計書"
---

最終更新: 2026-07-21

## 1. 概要

`members.json` に格納される `Member`（メンバー）エンティティのデータ設計。
`Member` は他の全機能（リリース紐付け・プレイリスト・ユニット・ライブ・関係性マップ・タイムライン等）から参照される最も基盤的なエンティティだが、専用のデータ設計書がこれまで存在しなかった（#1379）。

---

## 2. 背景・目的

| 目的 | 詳細 |
| --- | --- |
| `Member` 本体のフィールド定義の一元化 | `types/member.ts` の `MemberSchema` に散在するフィールドの意味・業務ルールをまとめる |
| 機能固有ドキュメントとの役割分担 | フィールドごとの詳細な算出ロジック・UI仕様は既存の機能別設計書に記載済みのため、本書では概要とリンクに留め重複を避ける |

---

## 3. 型定義

### `Member` インターフェース（`types/member.ts`）

```typescript
export const MemberSchema = z.object({
  id: z.string().min(1),
  name: z.string().min(1),
  ruby: z.string().min(1),
  status: z.string().min(1),
  generation: z.number(),
  color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
  colorName: z.string().optional(),
  leaderGenerationNumber: z.number().int().positive().nullable().optional(),
  nickname: z.array(z.string()).optional(),
  joiningRoute: z.string().optional(),
  latestStatus: StatusEntrySchema,
  officialSns: z.array(SnsEntrySchema).optional(),
  statusHistory: z.array(StatusEntrySchema).optional(),
  snsCheck: SnsCheckSchema.optional(),
  youtubeChannel: YoutubeChannelSchema.optional(),
  tiktokChannel: TikTokChannelSchema.optional(),
  profile: z.object({
    dob: z.string(),
    joinDate: z.string(),
    gradDate: z.string().nullable(),
    family: z.string(),
    birthplace: z.string().optional(),
    instructor: z.array(z.string()).optional(),
  }),
});
export type Member = z.infer<typeof MemberSchema>;
```

### フィールド詳細

| フィールド | 型 | 必須 | 説明 |
| --- | --- | --- | --- |
| `id` | `string` | ✅ | メンバーの識別子（kebab-case。例: `iida-kaori`）。他エンティティから `memberId` として参照される主キー |
| `name` | `string` | ✅ | 氏名 |
| `ruby` | `string` | ✅ | ふりがな（五十音ソート等に使用） |
| `status` | `string` | ✅ | 在籍状況を表す短い文字列（例: 現役メンバーの所属ユニット名、卒業メンバーの `卒業`） |
| `generation` | `number` | ✅ | 期数 |
| `color` | `string`（`#RRGGBB`） | — | メンバーカラー（HEX）。バッジ表示のスタイル決定に使用。詳細 → [メンバーカラーバッジ設計書](/design/member/member-color-badge-design) |
| `colorName` | `string` | — | メンバーカラーの表示名（例: `ライトピンク`）。未設定時は `color` の HEX 値をそのまま表示 |
| `leaderGenerationNumber` | `number`（正整数） | — | リーダー在任時の「代」番号。関係性マップのリーダー継承エッジ算出に使用。詳細 → [メンバー関係性マップ データ設計書](/design/member/member-map-data-design) |
| `nickname` | `string[]` | — | 愛称（複数バリエーションを持つメンバーがいるため配列）。詳細 → [メンバー関係性マップ データ設計書](/design/member/member-map-data-design) §9 |
| `joiningRoute` | `string` | — | 加入ルート（オーディション名等）。詳細 → [メンバー関係性マップ データ設計書](/design/member/member-map-data-design) §10 |
| `latestStatus` | `StatusEntry` | ✅ | 最新の近況エントリ。詳細 → [Amebaブログ機能 データ設計書](/design/sns/ameba-data-design) |
| `officialSns` | `SnsEntry[]` | — | 公式 SNS リンク一覧 |
| `statusHistory` | `StatusEntry[]` | — | 近況の変更履歴（新しい順）。保持ルールは §5 を参照 |
| `snsCheck` | `SnsCheck` | — | 近況自動検索（`sync-status`）の直近チェック結果 |
| `youtubeChannel` | `YoutubeChannel` | — | 個人 YouTube チャンネル情報。解決ロジックは §5 を参照 |
| `tiktokChannel` | `TikTokChannel` | — | 個人 TikTok アカウント情報。解決ロジックは §5 を参照（#1478） |
| `profile.dob` | `string` | ✅ | 生年月日 |
| `profile.joinDate` | `string`（`YYYY-MM-DD`） | ✅ | 加入日。`MemberReleaseLink` の実質基準日算出に使用。詳細 → [リリース データ設計書](/design/common/release-data-design) |
| `profile.gradDate` | `string \| null` | ✅ | 卒業日。現役メンバーは `null` |
| `profile.family` | `string` | ✅ | 出身グループ・ユニット等（自由記述） |
| `profile.birthplace` | `string` | — | 出身地。ライブ機能の「ご当地ライブ」判定に使用（§6の Neon 側 `Member` と対）。詳細 → [ライブ・フェス機能 テーブル設計書](/design/live/live-data-design) |
| `profile.instructor` | `string[]` | — | 師匠にあたるメンバーの `id` 一覧。詳細 → [メンバー関係性マップ データ設計書](/design/member/member-map-data-design) §「師弟関係」 |

### `StatusEntry` インターフェース（`Member.latestStatus` / `Member.statusHistory` の要素）

```typescript
export const StatusEntrySchema = z.object({
  content: z.string(),
  lastUpdated: z.string(),
  sources: z.array(z.string()),
});
```

| フィールド | 型 | 説明 |
| --- | --- | --- |
| `content` | `string` | 近況本文 |
| `lastUpdated` | `string`（`YYYY-MM-DD`） | 最終更新日（JST） |
| `sources` | `string[]` | 情報源 URL 一覧 |

### `SnsEntry` インターフェース（`Member.officialSns` の要素）

```typescript
export const SnsEntrySchema = z.object({
  url: z.string(),
  active: z.boolean(),
});
```

### `SnsCheck` インターフェース（`Member.snsCheck`）

```typescript
export const SnsCheckSchema = z.object({
  status: z.enum(['ok', 'error', 'skipped', 'unknown']),
  hasRecentUpdate: z.boolean(),
  lastCheckedAt: z.string(),
  reason: z.string().optional(),
  lastPostAt: z.string().optional(),
});
```

| フィールド | 型 | 説明 |
| --- | --- | --- |
| `status` | `'ok' \| 'error' \| 'skipped' \| 'unknown'` | 直近の自動検索（Exa Search API）の実行結果 |
| `hasRecentUpdate` | `boolean` | 直近の検索で近況の更新が見つかったか |
| `lastCheckedAt` | `string` | 最終チェック日時 |
| `reason` | `string` | `status` が `error`/`skipped` の場合の理由（任意） |
| `lastPostAt` | `string` | 検出した最新投稿の日時（任意） |

### `YoutubeChannel` インターフェース（`Member.youtubeChannel`、`types/youtube.ts`）

```typescript
export const YoutubeChannelSchema = z.object({
  handle: z.string().min(1),
  playlistId: z.string().min(1),
});
```

| フィールド | 型 | 説明 |
| --- | --- | --- |
| `handle` | `string` | YouTube チャンネルハンドル（`@` 始まり） |
| `playlistId` | `string` | ハンドルから解決されたアップロード動画プレイリスト ID（サーバー側で自動解決。§5参照） |

### `TikTokChannel` インターフェース（`Member.tiktokChannel`、`types/tiktok.ts`）

```typescript
export const TikTokChannelSchema = z.object({
  username: z.string().min(1),
});
```

| フィールド | 型 | 説明 |
| --- | --- | --- |
| `username` | `string` | TikTok のユーザー名（`@` を含まない。RapidAPI の `unique_id` パラメータにそのまま渡せる値） |

TikTok は RapidAPI（`tiktok-scraper7.p.rapidapi.com`）が `unique_id`（username）を直接指定して投稿取得できるため、YouTube の `playlistId` のようなサーバー側 API 解決は不要（§5参照）。詳細 → [TikTok投稿機能 データ設計書](/design/sns/tiktok-data-design)

---

## 4. データファイル

### members.json（Vercel Blob）

- Vercel Blob（`MEMBERS_BLOB_URL`）で管理。ローカルにフリーズドコピーは持たない（読み取りは Blob URL への `fetch`）
- 更新経路: `PUT /api/members`（メンバー詳細画面の編集フォーム）、`sync-members` ワークフロー（近況自動検索・Ameba同期）、各種 `scripts/patch/patch-*.ts`（愛称・加入ルート・メンバーカラー・リーダー代・師匠情報等の一括投入）

---

## 5. `PUT /api/members` の業務ルール

`app/api/members/route.ts` が Blob への書き込みを一元的に担う。クライアントから送信されたペイロードの一部フィールドはサーバー側で上書き・算出され、クライアント値に依存しない。

### 5-1. `statusHistory` の保持ルール

- `latestStatus.content` が既存値から変化した場合のみ、新しい `StatusEntry` を `statusHistory` の先頭に追加する
- 変化の有無に関わらず、保存前に以下の2条件でフィルタする（**早い方を適用**）:
  - 30日以内（`lastUpdated` が現在日から30日以内）
  - 100件以内（`slice(0, 100)`）
- `latestStatus.lastUpdated` は JST（`Asia/Tokyo`）でサーバー側が生成する。クライアントが送った値は無視される（#141）

### 5-2. `youtubeChannel` の解決ロジック

| 条件 | 動作 |
| --- | --- |
| `handle` が空 | `youtubeChannel` フィールドごと削除する |
| `handle` が既存値と同じ | 既存の `playlistId` を引き継ぐ（YouTube API クォータ節約のため再解決しない） |
| `handle` が新規・変更あり | `fetchPlaylistId(handle)` で YouTube API から `playlistId` を解決する。解決失敗時は 422 エラーを返し保存しない |

### 5-3. `tiktokChannel` の解決ロジック（#1478）

| 条件 | 動作 |
| --- | --- |
| `username` が空 | `tiktokChannel` フィールドごと削除する |
| `username` が指定あり | トリムしてそのまま保存する（API 解決不要。§3参照） |

---

## 6. Neon 側「出身地照合用メンバー」（`Member`）

ライブ機能の「ご当地ライブ」判定でのみ使用される、Blob `Member` とは別の Prisma モデル。

```prisma
model Member {
  memberId   String  @id @db.VarChar(50)
  birthplace String? @db.VarChar(255)

  @@map("members")
}
```

- `memberId` は Blob `Member.id` と同値を Neon 側に複製したもの（DB レベルの FK 制約はない）
- `birthplace` のみを保持する部分的なミラーであり、Blob `Member` の完全な複製ではない
- 詳細 → [ライブ・フェス機能 テーブル設計書](/design/live/live-data-design)

---

## 7. ER 図

→ [ER 図](/design/common/er-diagram) を参照すること。`Member` はコア音楽データ・ライブ/フェス・メンバー関係性マップの各セクションに登場する。

---

## 8. 関連設計書

| 設計書 | 本書との関係 |
| --- | --- |
| [Amebaブログ機能 データ設計書](/design/sns/ameba-data-design) | `latestStatus`/`statusHistory` の RSS 取得・Topページ新着判定の詳細 |
| [メンバーカラーバッジ設計書](/design/member/member-color-badge-design) | `color`/`colorName` を用いたバッジスタイル算出ロジック |
| [メンバー関係性マップ データ設計書](/design/member/member-map-data-design) | `nickname`・`joiningRoute`・`leaderGenerationNumber`・`profile.instructor` の算出・表示ロジック |
| [リリース データ設計書](/design/common/release-data-design) | `profile.joinDate`/`profile.gradDate` を用いた `MemberReleaseLink` 紐付けロジック |
| [ライブ・フェス機能 テーブル設計書](/design/live/live-data-design) | Neon 側「出身地照合用メンバー」（`Member`）・ご当地ライブ判定 |
| [TikTok投稿機能 データ設計書](/design/sns/tiktok-data-design) | `tiktokChannel` を用いたOGメンバー個人アカウント収集ロジック |

---

## 改訂履歴

| 版 | 更新日 | 変更内容 |
| --- | --- | --- |
| 1.1 | 2026-08-09 | OGメンバーのTikTok個人アカウント収集に伴い `tiktokChannel` フィールドを追加（§3・§5-3・§8）。YouTubeの `youtubeChannel` と異なりAPI側のID解決が不要なため `username` のみを保持する（#1478） |
| 1.0 | 2026-07-21 | 初版作成（#1379） |
