コンテンツにスキップ
Documents for MorningStatusApp
Esc
↑↓移動↵開く⌘Jプレビュー
このページの内容

メンバー データ設計書

最終更新: 2026-09-17

1. 概要

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


2. 背景・目的

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

3. 型定義

Member インターフェース(types/member.ts)

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)。バッジ表示のスタイル決定に使用。詳細 → メンバーカラーバッジ設計書
colorName string — メンバーカラーの表示名(例: ライトピンク)。未設定時は color の HEX 値をそのまま表示
leaderGenerationNumber number(正整数) — リーダー在任時の「代」番号。関係性マップのリーダー継承エッジ算出に使用。詳細 → メンバー関係性マップ データ設計書
nickname string[] — 愛称(複数バリエーションを持つメンバーがいるため配列)。詳細 → メンバー関係性マップ データ設計書 §9
joiningRoute string — 加入ルート(オーディション名等)。詳細 → メンバー関係性マップ データ設計書 §10
latestStatus StatusEntry ✅ 最新の近況エントリ。詳細 → Amebaブログ機能 データ設計書
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 の実質基準日算出に使用。詳細 → リリース データ設計書
profile.gradDate string | null ✅ 卒業日。現役メンバーは null
profile.family string ✅ 出身グループ・ユニット等(自由記述)
profile.birthplace string — 出身地。ライブ機能の「ご当地ライブ」判定に使用(§6の Neon 側 Member と対)。詳細 → ライブ・フェス機能 テーブル設計書
profile.instructor string[] — 師匠にあたるメンバーの id 一覧。詳細 → メンバー関係性マップ データ設計書 §「師弟関係」

StatusEntry インターフェース(Member.latestStatus / Member.statusHistory の要素)

export const StatusEntrySchema = z.object({
  content: z.string(),
  lastUpdated: z.string(),
  sources: z.array(z.string()),
});
フィールド 型 説明
content string 近況本文
lastUpdated string 実投稿日時。自動収集のうちAmeba RSS由来は時刻まで保持したISO 8601文字列、それ以外(Ameba以外の自動Web検索収集・手動編集)はYYYY-MM-DD(時刻情報なし)の2パターンが混在する。日付単位(JST暦日)で扱う場合は呼び出し側でlib/date.tsのtoJstDateString()により丸める(#1502)
sources string[] 情報源 URL 一覧

SnsEntry インターフェース(Member.officialSns の要素)

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

SnsCheck インターフェース(Member.snsCheck)

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)

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)

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投稿機能 データ設計書


4. データファイル

members.json(Vercel Blob)

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

5. /api/members の設計

app/api/members/route.ts が Vercel Blob(members.json)への読み書きを一元的に担う。

5-1. GET /api/members: 一覧取得とキャッシュ戦略

  • fetch(blobUrl, { next: { revalidate: 3600 } }) で取得したBlobの生JSON({ members: Member[] })をそのまま返す。getMembersFromBlob()(lib/blob.ts)と同じ revalidate: 3600 を使い、Blobアクセス経路間でキャッシュ戦略を統一している(もともと PUT の cache: "no-store" 設定を流用していたが#1296で是正した経緯がある)
  • /member-map(クライアント側 fetch)・/members(メンバー一覧、TanStack Queryの useQuery、#1640)の2画面がこのエンドポイントをクライアント側から参照する
  • Issue #1569 で導入した UnifiedFeedItem 判別共用体パターン(SNS4種の異なる形状を統合する型)とは異なり、Member は元々単一形状のドメイン型として types/member.ts に存在するため、本エンドポイントに新規の判別共用体スキーマは導入しない。既存の Member 型をそのまま利用する
  • /members 画面はサーバー側 page.tsx で prefetchQuery(クエリキー ['members'])→dehydrate→HydrationBoundary を行い、初回表示のちらつきを回避する(#1569と同じ構成)。一覧描画部分はクライアントコンポーネントへ切り出し、useQuery で取得したレスポンスの .members を参照する

PUT /api/members はクライアントから送信されたペイロードの一部フィールドをサーバー側で上書き・算出する。クライアント値に依存しない。

5-2. PUT /api/members: statusHistory の保持ルール

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

5-3. PUT /api/members: youtubeChannel の解決ロジック

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

5-4. PUT /api/members: tiktokChannel の解決ロジック(#1478)

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

6. Neon 側「出身地照合用メンバー」(Member)

ライブ機能の「ご当地ライブ」判定でのみ使用される、Blob Member とは別の 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 の完全な複製ではない
  • 詳細 → ライブ・フェス機能 テーブル設計書

7. ER 図

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


8. 関連設計書

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

改訂履歴

版 更新日 変更内容
1.3 2026-09-17 GET /api/membersの一覧取得・キャッシュ戦略を明記し、/members画面のTanStack Query導入方針を追加。既存の§5(PUT業務ルール)を/api/membersの設計として再編(§5-1にGETを追加、§5-2〜5-4にPUTの既存内容を移動)(#1640)
1.2 2026-08-15 StatusEntry.lastUpdated の説明を修正。morning-status-app PR #1504(#1502対応)でAmebaのparsePubDateが時刻情報を切り捨てていたバグが是正され、自動収集のうちAmeba RSS由来は時刻付きISO 8601文字列・それ以外(Ameba以外の自動Web検索収集・手動編集)はYYYY-MM-DDの2パターンが混在する仕様になったが、本ファイルの記述がこれまで追随していなかったための追補
1.1 2026-08-09 OGメンバーのTikTok個人アカウント収集に伴い tiktokChannel フィールドを追加(§3・§5-3・§8)。YouTubeの youtubeChannel と異なりAPI側のID解決が不要なため username のみを保持する(#1478)
1.0 2026-07-21 初版作成(#1379)

このページは役に立ちましたか?