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

ライブ・フェス機能 テーブル設計書

最終更新: 2026-09-22 関連 Issue: #528, #776, #775, #1519, #1545


1. 概要

Vercel Postgres(Neon)上に構築するライブ機能のテーブル設計書。 #527 の結論(Neon 採用・react-leaflet・JIS 2桁コード)を前提とする。

対象テーブル

テーブル 種別 概要
venues 新規 会場(住所・座標)
tours 新規 ツアー
lives 新規 公演(日程・会場)
member_lives 新規 メンバーと公演の紐づけ
member_festivals 新規 メンバーとフェス出演の紐づけ
prefecture_codes 新規 都道府県コード変換テーブル
members 新規 members.json 連携先(出身地照合用)
setlists 新規 セットリスト(ツアー・フェス別演奏曲目)

データストア方針

  • ライブ関連データ(本設計書の対象): Neon(Vercel Postgres) を正とする
  • 現行データ(メンバー・リリース等): Vercel Blob / JSON を継続
  • メンテナンス: VSCode で CSV を直接編集し、シードスクリプトで Neon に投入(#609)

2. テーブル定義

2-1. venues(会場)

会場の基本情報と react-leaflet 表示用の座標を管理する。

CREATE TABLE venues (
  venue_id    VARCHAR(8)    PRIMARY KEY,
  name        VARCHAR(255)  NOT NULL,
  prefecture  CHAR(2)       NOT NULL,
  latitude    DECIMAL(9,6),
  longitude   DECIMAL(9,6)
);
カラム名 型 NOT NULL 説明
venue_id VARCHAR(8) ✅ PK。都道府県 JIS 2桁 + 連番 3桁(例: 13001)
name VARCHAR(255) ✅ 会場名(例: 日本武道館)
prefecture CHAR(2) ✅ JIS 2桁コード(例: 13)。先頭ゼロ保持のため文字列型
latitude DECIMAL(9,6) — 緯度(WGS84。例: 35.693200)。未登録時は NULL
longitude DECIMAL(9,6) — 経度(WGS84。例: 139.749700)。未登録時は NULL

備考:

  • DECIMAL(9,6) は整数部 3桁・小数部 6桁(精度 約 0.1m)
  • PostGIS 不使用(精細な地図表示は不要という #527 の方針に準拠)
  • 座標未登録の会場はマップ上に表示しない

インデックス:

CREATE INDEX idx_venues_prefecture ON venues (prefecture);

2-2. tours(ツアー)

ツアー単位の情報を管理する。

CREATE TABLE tours (
  tour_id     VARCHAR(20)   PRIMARY KEY,
  title       VARCHAR(255)  NOT NULL,
  release_id  VARCHAR(255)
);
カラム名 型 NOT NULL 説明
tour_id VARCHAR(20) ✅ PK。年 + 識別子(例: 2026B)
title VARCHAR(255) ✅ ツアー名称(例: モーニング娘。'26 コンサートツアー春 – Rays Of Light –)
release_id VARCHAR(255) — 関連リリース ID(Neon releases の id と対応。FK 制約なし)

備考:

  • release_id は releases.json が Neon の Release/Track テーブルへ移行済み(MorningStatusApp #854)であるため実際は同一 DB 内の参照だが、現状は DB レベルの FK 制約は設けていない(詳細は「Blobデータとの FK 制約」参照)

2-3. lives(公演)

個別公演の日程・会場・所属ツアーを管理する。

CREATE TABLE lives (
  live_id   VARCHAR(20)   PRIMARY KEY,
  tour_id   VARCHAR(20)   REFERENCES tours (tour_id),
  venue_id  VARCHAR(8)    NOT NULL REFERENCES venues (venue_id),
  date      DATE          NOT NULL,
  time      TIME,
  title     VARCHAR(255)
);
カラム名 型 NOT NULL 説明
live_id VARCHAR(20) ✅ PK。ツアー ID + 連番 3桁(例: 2026B001)
tour_id VARCHAR(20) — FK → tours(tour_id)。単独公演は NULL
venue_id VARCHAR(8) ✅ FK → venues(venue_id)
date DATE ✅ 公演日(例: 2026-04-11)
time TIME — 開演時刻(例: 13:00:00)。00:00 は時刻不明を意味し、画面上は時刻を表示しない
title VARCHAR(255) — 公演タイトル。ツアー名と異なる場合のオーバーライド用

インデックス:

CREATE INDEX idx_lives_tour_id  ON lives (tour_id);
CREATE INDEX idx_lives_venue_id ON lives (venue_id);
CREATE INDEX idx_lives_date     ON lives (date);

2-4. member_lives(メンバー × 公演紐づけ)

メンバーと公演の多対多関係を管理する(#517 で使用)。

CREATE TABLE member_lives (
  member_id  VARCHAR(50)   NOT NULL,
  live_id    VARCHAR(20)   NOT NULL REFERENCES lives (live_id),
  PRIMARY KEY (member_id, live_id)
);
カラム名 型 NOT NULL 説明
member_id VARCHAR(50) ✅ members.json の id と対応。FK 制約なし
live_id VARCHAR(20) ✅ FK → lives(live_id)

備考:

  • member_id は Blob 管理の members.json を参照するため、DB レベルの FK 制約は設けない
  • PK = (member_id, live_id) の複合主キーで重複を防ぐ

インデックス:

CREATE INDEX idx_member_lives_live_id ON member_lives (live_id);

生成タイミング・書き込み経路:

member_lives は、フェス・ツアー登録バッチ(バッチ設計書 O4「フェス・ツアー登録バッチ」)が公演(lives)を登録するタイミングで、登録対象の公演日が在籍期間(joinDate 〜 gradDate。gradDate が未設定=現役の場合は上限なし)に含まれる全メンバーについて生成する。live_id への FK のみを持つ設計(上表参照)のため、フェス(festivals)は対象外。対象メンバーの算出・書き込みロジックは scripts/lib/member-live-sync.ts に切り出し、O4バッチと一回限りの再同期パッチ(後述)の両方から共有する。書き込みは skipDuplicates を指定した createMany で行い、PK(member_id, live_id)の重複判定は公演自体の重複判定(日程・会場)とは独立して行う。

備考(旧仕様・#1619で是正): 以前は member_lives への書き込みが、メンバー詳細画面初回表示時(app/members/[id]/page.tsx)の遅延初期化のみに依存しており、そのメンバーの member_lives が1件も無い場合に限り在籍期間内の公演を一括バックフィルする設計だった(#654)。フェス・ツアー登録バッチ側では member_lives へ一切書き込んでいなかったため、一度でも画面表示済み(member_lives に1件でも存在)のメンバーは、以降どれだけ新しい公演を登録しても member_lives に反映されない不具合があった。この遅延初期化ロジックは撤去し、書き込みは登録バッチ側に一本化した。


2-5. prefecture_codes(都道府県コード変換)

members.birthplace(自由文字列)を JIS 2桁コードに変換するための参照テーブル(#520 ご当地ライブ検出で使用)。

CREATE TABLE prefecture_codes (
  prefecture_code  CHAR(2)       PRIMARY KEY,
  name             VARCHAR(20)   NOT NULL,
  aliases          TEXT[]
);
カラム名 型 NOT NULL 説明
prefecture_code CHAR(2) ✅ PK。JIS 2桁コード(例: 13)
name VARCHAR(20) ✅ 都道府県名(例: 東京都)
aliases TEXT[] — 別表記の配列(例: {"東京", "東京 ", "Tokyo"})

備考:

  • aliases は birthplace の表記ゆれ吸収のために使用(部分一致マッチング)
  • 初期データ: 47都道府県分を CSV(prefecture.csv)からシード投入
  • 海外拡張コード(#674): 海外公演対応のため以下3コードを追加(aliases は空)
prefecture_code name 対象地域
50 アジア 韓国・台湾・香港・中国など
60 ヨーロッパ フランスなど
70 北中米 米国・メキシコなど

2-6. members(出身地照合用)

Blob の members.json から出身地情報のみを複製し、会場の都道府県との照合に使用する(#520 専用テーブル)。

CREATE TABLE members (
  member_id   VARCHAR(50)   PRIMARY KEY,
  birthplace  VARCHAR(255)
);
カラム名 型 NOT NULL 説明
member_id VARCHAR(50) ✅ PK。members.json の id と一致
birthplace VARCHAR(255) — 出身地の自由文字列(例: 東京都港区)。未設定時は NULL

備考:

  • members.json の profile.birthplace を定期同期または手動投入
  • 都道府県コードへの変換は prefecture_codes テーブルとのマッチングでアプリ層が担当
  • この members テーブルは Blob の members.json を置き換えるものではない

2-7. festivals(フェス)

参加フェスの開催情報を管理する。複数日開催フェスのうち参加する1日を1レコードとして管理する。

CREATE TABLE festivals (
  festival_id  VARCHAR(50)   PRIMARY KEY,
  name         VARCHAR(255)  NOT NULL,
  date         DATE          NOT NULL,
  time         TIME,
  performance_time TIME,
  venue_id     VARCHAR(8)    NOT NULL REFERENCES venues (venue_id)
);
カラム名 型 NOT NULL 説明
festival_id VARCHAR(50) ✅ PK。例: 2025-ROCKINJAPAN-1
name VARCHAR(255) ✅ フェス名(例: ROCK IN JAPAN FESTIVAL 2025)
date DATE ✅ 参加日
time TIME — 開催時刻(フェス全体の開演時刻。例: 10:00:00)。00:00 は時刻不明を意味し、画面上は時刻を表示しない(lives.time と同じ運用ルール)
performance_time TIME — 出演時刻(モーニング娘。の出演時刻。例: 15:30:00)。00:00 は時刻不明を意味し、画面上は時刻を表示しない
venue_id VARCHAR(8) ✅ FK → venues(venue_id)

インデックス:

CREATE INDEX idx_festivals_venue_id ON festivals (venue_id);
CREATE INDEX idx_festivals_date     ON festivals (date);

時刻の運用ルール(MorningStatusApp#1545):

  • time(開催時刻)・performance_time(出演時刻)とも、時刻を持たない既存のフェスのレコードに影響しないよう、NULL 許容で追加する(既存レコードは両方とも NULL のまま)。lives.time と同じく、NULL と 00:00 はどちらも時刻不明として扱い、画面上は時刻を表示しない
  • 出演時刻はモーニング娘。の出演時刻で、フェス全体の開催時刻とは別に持つ。フェス詳細画面は両方を別々に表示する。一方、タイムラインと一覧系の画面(フェス一覧・都道府県別公演画面のフェス一覧・メンバー詳細画面のご当地ライブ/関連ライブ・トップ画面の過去のこの日)に載せる時刻は、ライブの時刻と同じく1つにするため、出演時刻が有効(NULL・00:00 以外)ならその値、そうでなければ開催時刻を使う(以下「代表時刻」。タイムラインでの導出はイベントテーブル定義書 §5-6を参照)
  • 登録バッチ(バッチ設計書 O4「フェス・ツアー登録バッチ」)は、フェスの新規登録時に両方の時刻を受け取り、登録済みのフェスに対しては時刻のみを後から更新できる

2-8. setlists(セットリスト)

ツアーまたはフェスの演奏曲目・順序を管理する(#775)。

CREATE TABLE setlists (
  setlist_id   VARCHAR(80)   PRIMARY KEY,
  type         VARCHAR(10)   NOT NULL,
  tour_id      VARCHAR(20)   REFERENCES tours (tour_id) ON DELETE SET NULL,
  festival_id  VARCHAR(50)   REFERENCES festivals (festival_id) ON DELETE SET NULL,
  seq          INT           NOT NULL
);
カラム名 型 NOT NULL 説明
setlist_id VARCHAR(80) ✅ PK。{tourId or festivalId}-{3桁連番}(例: T2025-01-001)
type VARCHAR(10) ✅ 種別: tour or festival
tour_id VARCHAR(20) — FK → tours(tour_id)。ツアーセットリストの場合に設定
festival_id VARCHAR(50) — FK → festivals(festival_id)。フェスセットリストの場合に設定
seq INT ✅ 演奏順(1始まり)

備考:

  • tour_id と festival_id は排他的(いずれか一方のみ設定)
  • 曲の識別子(trackId)は本テーブルの列では持たず、§2-9「setlist_tracks」の中間テーブルで管理する(1曲が複数トラックに正しく該当しうるため。詳細はラジオオンエア データ設計書 §4-0参照)
  • 画面表示時は紐づく trackId を Neon の releases/tracks テーブルで解決し、曲名・YouTube 動画 ID を取得する(旧仕様の記載: 以前は「Blob管理のreleases.jsonを参照する」としていたが、releases.json は MorningStatusApp #854 でNeonのRelease/Trackテーブルへ移行済みのため実態と乖離していた記載を是正)

インデックス:

CREATE INDEX idx_setlists_tour_id     ON setlists (tour_id);
CREATE INDEX idx_setlists_festival_id ON setlists (festival_id);

2-9. setlist_tracks(セットリスト × トラック紐付け)

1件の setlists レコード(セットリスト内の1曲)が複数の tracks(同一タイトルのCD盤・配信版シングル等)に該当しうるための中間テーブル。ラジオ側のラジオオンエア データ設計書 §2-5「radio_onair_song_tracks」と同じ設計パターン。

CREATE TABLE setlist_tracks (
  setlist_id VARCHAR(80)   NOT NULL REFERENCES setlists (setlist_id) ON DELETE CASCADE,
  track_id   VARCHAR(255)  NOT NULL REFERENCES tracks (track_id),
  PRIMARY KEY (setlist_id, track_id)
);
CREATE INDEX idx_setlist_tracks_track_id ON setlist_tracks (track_id);

releases/tracks は MorningStatusApp 側の Prisma スキーマ(Release/Track モデル)で管理されている同一 Neon DB 内のテーブルであり、radio_onair_song_tracks(§2-5)と同様に実FK制約を設ける。


2-10. member_festivals(メンバー × フェス出演紐づけ)

メンバーとフェス出演の多対多関係を管理する(Tatsukiyoshi/MorningStatusApp#1621)。member_lives(§2-4)と対になる設計とし、festival_idのみへの FK を持つ。

CREATE TABLE member_festivals (
  member_id    VARCHAR(50)   NOT NULL,
  festival_id  VARCHAR(50)   NOT NULL REFERENCES festivals (festival_id),
  PRIMARY KEY (member_id, festival_id)
);
カラム名 型 NOT NULL 説明
member_id VARCHAR(50) ✅ members.json の id と対応。FK 制約なし
festival_id VARCHAR(50) ✅ FK → festivals(festival_id)

備考:

  • member_id は Blob 管理の members.json を参照するため、DB レベルの FK 制約は設けない
  • PK = (member_id, festival_id) の複合主キーで重複を防ぐ

インデックス:

CREATE INDEX idx_member_festivals_festival_id ON member_festivals (festival_id);

設計判断: member_lives の汎用化ではなく新規テーブルを採用

「ライブ(公演)」全般を指すモデルへ member_lives を汎用化する案(live_id・festival_id の両カラムを持たせ、いずれか一方のみ非 NULL とする CHECK 制約を課す)も検討したが、不採用とした。この種の制約(@@index・@@unique で表現できないインデックス・制約)は、Prisma 7.4系以降の既知の不具合により prisma migrate dev 実行のたびに誤った DROP が生成されるため(MorningStatusApp CLAUDE.md「Prisma利用時の制約」)、member_lives と対になる member_festivals を新設する方式とした。

生成タイミング・書き込み経路:

member_festivals は、フェス・ツアー登録バッチ(バッチ設計書 O4「フェス・ツアー登録バッチ」)がフェス出演(festivals)を登録するタイミングで、登録対象の開催日が在籍期間(joinDate 〜 gradDate。gradDate が未設定=現役の場合は上限なし)に含まれる全メンバーについて生成する。対象メンバーの算出・書き込みロジックは、member_lives(§2-4)向けの scripts/lib/member-live-sync.ts と対になる形で scripts/lib/member-festival-sync.ts に切り出す想定。既存フェスデータ分は、member_lives のとき(#1619)と同様に一回限りのバックフィルパッチで別途補完する。


3. ER 図(ライブ機能テーブル)


4. CSV 投入フォーマット(シード用)

#609(Neon スキーマ実装)でのシード投入に使用する CSV フォーマット。

venues.csv

venueId,name,prefecture,latitude,longitude
13001,日本武道館,13,35.693200,139.749700

tours.csv

tourId,title,releaseId
2026B,モーニング娘。'26 コンサートツアー春 – Rays Of Light –,

lives.csv

tourId,liveId,date,time,venueId
2026B,2026B001,2026-04-11,13:00,13002

prefecture.csv

prefecture,name
01,北海道
13,東京都

festivals.csv

festivalId,name,date,venueId
2025-SUMMERSONIC-TK,SUMMER SONIC 2025(東京),2025/8/16,11005

time・performanceTime は任意項目のため、シード用 CSV には含めない(NULL で投入する)。

setlists.csv

setlistId,type,tourId,festivalId,seq,trackId

5. 設計上の注意点

ID採番ルール(tour_id / festival_id)

フェス・ツアー登録バッチ(バッチ設計書 O4)での自動採番ルール。採番アルゴリズム自体の詳細(スラッグ抽出処理等)はバッチ設計書側を参照。

tour_id: 年 + 季節コード(4桁+1桁、例: 2026D)。季節コードは以下の対応で、同一年・同一季節にツアーが重なる運用は想定しないため衝突時のサフィックス処理は不要。

季節コード 季節
A 冬
B 春
C 夏
D 秋

festival_id: 年 - スラッグ - 連番(例: 2025-ROCKINJAPAN-1)。スラッグはフェス名からラテン文字・数字のみを抽出して自動導出し、一意性は年+連番側で担保する(スラッグ自体の重複は許容する)。

prefecture フィールドの型

CHAR(2) を使用し、先頭ゼロを文字列として保持する。INTEGER や SMALLINT にすると 01 → 1 に変換されて照合が崩れる。

Blob データとの FK 制約

member_id(members.json 参照)は Blob 管理のデータを参照するため、DB レベルの外部キー制約を設けない。整合性はアプリ層で担保する。

release_id(tours.release_id)は releases.json が Neon の Release/Track テーブルへ移行済み(MorningStatusApp #854)であるため実際は同一 DB 内の参照だが、現状は FK 制約を設けていない(旧仕様の記載を是正。移行前は Blob 参照のため FK なしという理由が正しかったが、移行後も踏襲されたままになっていた)。一方、setlist_tracks.track_id(§2-9)は新設にあたり実 FK 制約を設けている。

座標精度

DECIMAL(9,6) は整数部 3桁 + 小数部 6桁で、精度 約 0.11m(11cm)。react-leaflet での表示用途に十分。PostGIS 導入は不要。


6. BFF層設計(Route Handler + TanStack Query)

採用要否の判断基準・全ドメイン共通の適用判定ログはデータ設計書(全体概要) §4・統合フィードアイテム データ設計書 §3を参照。Route Handler設計・TanStack Query導入手順・prefetch/hydrateの一般的な実装方法は同設計書 §4を参照し、本節ではライブ・フェスドメイン固有の差分(対象画面・スキーマ・エンドポイント)のみ記載する。対象画面は2つあり、6-1・6-2でそれぞれ設計する。

6-1. 都道府県別公演画面(/lives/prefecture/[code])

対象画面

都道府県別公演(/lives/prefecture/[code]、ライブ・フェス機能 画面設計書 §8)が対象。この画面は、公演(lives)とフェス出演(festivals)という異なる形状のデータを、地図のマーカー(フェス会場の座標も含む)と会場数の集計として1つの表示に統合しており(クライアント側でLiveFeedItem[]とFestivalFeedItem[]をLiveEventFeedItem[]として統合し、会場単位に集計する)、判断基準の「形状統合の要否」に該当するため。一覧は「フェス一覧」「公演一覧」の別セクションで、表示項目も種別ごとに異なるため、それぞれの配列をそのまま使う。

他のライブ・フェス関連画面(/lives・/lives/year/[year]・/lives/[id]・/festivals・/festivals/[id]・/lives/map・/lives/map/[region])は現状の実装で困っていない(単一形状のデータをServer Componentで直接取得するだけで足りている)ため変更しない。将来これらの画面のデータが複数画面から必要とされるようになった場合はあらためて判断する。

スキーマ(types/live.ts 想定)

kindフィールドを判別キーとする判別共用体として定義する。LiveEventFeedItemは会場単位の集計(地図マーカー・会場数)で両者を1つの配列として扱うための型で、一覧表示(フェス一覧・公演一覧)は種別ごとの型(LiveFeedItem・FestivalFeedItem)をそのまま使う。

const LiveEventFeedItemBaseSchema = z.object({
  id: z.string(),
  date: z.string(),        // "YYYY-MM-DD"
  venueId: z.string(),
  venueName: z.string(),
  venueLatitude: z.number().nullable(),
  venueLongitude: z.number().nullable(),
});

export const LiveFeedItemSchema = LiveEventFeedItemBaseSchema.extend({
  kind: z.literal('live'),
  time: z.string().nullable(),        // "HH:MM"。00:00・未設定は null
  tourId: z.string().nullable(),
  tourTitle: z.string().nullable(),
});
export type LiveFeedItem = z.infer<typeof LiveFeedItemSchema>;

export const FestivalFeedItemSchema = LiveEventFeedItemBaseSchema.extend({
  kind: z.literal('festival'),
  time: z.string().nullable(),        // "HH:MM"。代表時刻(出演時刻を優先し、不明なら開催時刻)。どちらも不明(00:00・未設定)は null
  name: z.string(),
});
export type FestivalFeedItem = z.infer<typeof FestivalFeedItemSchema>;

export const LiveEventFeedItemSchema = z.discriminatedUnion('kind', [
  LiveFeedItemSchema,
  FestivalFeedItemSchema,
]);
export type LiveEventFeedItem = z.infer<typeof LiveEventFeedItemSchema>;

備考: フィールド名(kind・id)は、メンバー詳細画面の関連ライブセクション(ライブ・フェス機能 画面設計書 §12.1、6-2)向けのRelatedLiveEntry型(types/live.ts)と揃えた。ただしRelatedLiveEntryはメンバー詳細画面専用のスキーマ、本節の型は都道府県別公演画面専用のAPIレスポンススキーマであり、統合せず別の型として並存させる(6-2参照。#1636設計時点で保留していた「統合をあらためて検討する」の結論)。venueLatitude/venueLongitudeは都道府県別公演画面の地図マーカー(会場座標)をクライアント側で組み立てるために持たせる。

API層

エンドポイント 責務 内部で呼ぶ既存関数
GET /api/lives?prefecture={code} 指定都道府県の公演をLiveFeedItem[]として返す prisma.live.findMany({ where: { venue: { prefecture: code } }, include: { venue: true, tour: true } })
GET /api/festivals?prefecture={code} 指定都道府県のフェス出演をFestivalFeedItem[]として返す prisma.festival.findMany({ where: { venue: { prefecture: code } }, include: { venue: true } })

いずれも既存のPATCH /api/lives/[id]・PATCH /api/festivals/[id](#1521デスクトップモード編集用)とは別ファイル(app/api/lives/route.ts・app/api/festivals/route.tsを新設)で、一覧取得専用。prefectureクエリパラメータは/lives/prefecture/[code]の[code]をそのまま渡す(都道府県コード未指定時の全件取得はこの画面では使用しないため対象外で、未指定は400を返す)。内部エラー時は500を返す(他の/api/feed/*と同様)。

prefectureの形式・存在の検証はAPI側では行わない。画面側で[code]の形式検証とprefecture_codesの存在確認を済ませ、不正時はnotFound()にするため、形式不正・未登録のコードが直接渡された場合は該当なしとして空配列(200)を返す。

API失敗時の画面表示: この画面では専用のエラー表示を設けない。サーバー側プリフェッチの失敗はNext.jsのエラー画面に、クライアント側の再取得の失敗はハイドレート済みのデータを表示し続ける挙動に委ねる。

並び順: 公演は日付昇順・同日は時刻昇順(00:00・未設定は末尾。lib/live-helpers.tsのsortLivesForNav)、フェスは日付昇順。並び替えはサーバー側(Prisma取得後、Route Handlerと画面のサーバー側プリフェッチが共有する変換関数内)で行い、クライアント側では行わない(従来の画面と同じ並び順を維持するため)。

マッピングルール

フィールド 導出元
id live.liveId / festival.festivalId
date live.date / festival.date(toISOString().split('T')[0]でYYYY-MM-DD化)
venueId・venueName venue.venueId・venue.name
venueLatitude・venueLongitude venue.latitude・venue.longitude(Decimal型をNumber()で変換)
time(ライブ) live.time(TIME型)。shouldShowTimeが真ならformatLiveTimeで"HH:MM"文字列に整形し、偽(00:00または未設定)の場合はnull
time(フェス) 代表時刻。festival.performanceTimeがshouldShowTime真ならそれを、偽ならfestival.timeを、いずれもライブと同じ判定・整形("HH:MM")で導出する。どちらも偽(00:00または未設定)の場合はnull
tourId・tourTitle(ライブのみ) live.tourId・live.tour?.title
name(フェスのみ) festival.name

6-2. 関連ライブセクション(/members/[id])

対象画面

メンバー詳細画面の関連ライブセクション(RelatedLivesSection、ライブ・フェス機能 画面設計書 §12.1)が対象。RelatedLiveGroup[]はkind: 'live'のツアーグループとkind: 'festival'のフェスグループを1つのソート済みリストに混在させており、判断基準の「形状統合の要否」に該当する(think-issue、MorningStatusApp#1654)。

6-1のLiveFeedItem/FestivalFeedItemは都道府県で絞り込むクエリだが、本画面が必要とするのはメンバーで絞り込むクエリ(member_lives・member_festivalsをキーに結合)であり、6-1のエンドポイントは流用できない。また6-1の型はグルーピング前のflatな配列を返す設計だが、本画面はグループ化済みのリストを必要とするため、スキーマ・エンドポイントとも6-1とは別に用意する(6-1の備考参照)。

スキーマ(types/live.ts 想定)

RelatedLiveEntry・RelatedLiveGroupをZodスキーマとして正式化する。既存の同名TypeScript型(Client Componentへのprops専用、Zod化されていなかった)をAPIレスポンスのスキーマへ格上げする。

export const RelatedLiveEntrySchema = z.object({
  kind: z.enum(['live', 'festival']),
  id: z.string(),
  date: z.string(),           // "YYYY/M/D"形式(表示用に整形済み)
  time: z.string().nullable(), // "HH:MM"形式。代表時刻(フェスは出演時刻を優先し、不明なら開催時刻)。どちらも不明(00:00・未設定)は null
  venueName: z.string(),
  prefName: z.string(),
  isHometown: z.boolean(),     // メンバーの出身地と会場都道府県が一致する場合 true
});
export type RelatedLiveEntry = z.infer<typeof RelatedLiveEntrySchema>;

export const RelatedLiveGroupSchema = z.object({
  kind: z.enum(['live', 'festival']),
  groupKey: z.string().nullable(), // kind: 'live' は tours.tourId(単独公演は null)、'festival' は festivals.name(常に非null)
  title: z.string().nullable(),    // ツアー名またはフェス名。単独公演の場合のみ null
  entries: z.array(RelatedLiveEntrySchema),
  firstDate: z.string(),            // グループ内最初の公演日。ソート用
});
export type RelatedLiveGroup = z.infer<typeof RelatedLiveGroupSchema>;

RelatedLiveEntryはkindによってフィールド構成が変わらない(idが指すレコードの意味とリンク先がkindで変わるのみ。判別共用体ではなく単一形状)ため、6-1のLiveEventFeedItem(z.discriminatedUnion)とは異なり、kindはz.enumとする。

API層

エンドポイント 責務 内部で呼ぶ既存関数
GET /api/members/{id}/related-lives 指定メンバーの関連ライブ・関連フェスを取得し、グルーピング・ご当地判定・日付/時刻整形まで済ませたRelatedLiveGroup[]を返す prisma.memberLive.findMany({ where: { memberId: id }, include: { live: { include: { venue: true, tour: true } } } })・prisma.memberFestival.findMany({ where: { memberId: id }, include: { festival: { include: { venue: true } } } })

現行app/members/[id]/page.tsxにある、上記2クエリの実行結果からグルーピング・ご当地判定・日付整形を行うロジック(RawRelatedLiveEntry・RawRelatedLiveGroup関連の処理)をRoute Handler側に移植する。ただしtimeの導出は現行実装(フェス出演は常にnull)から変更し、6-1と同じ代表時刻ロジック(shouldShowTime・出演時刻優先で開催時刻にフォールバック)を適用する仕様変更を行う。festivalsテーブルにはtime(開催時刻)・performance_time(出演時刻)カラムが存在するにもかかわらず本画面では未使用だったため、6-2の新設を機に6-1と同じ導出ロジックに揃え、フェス出演にも時刻を表示できるようにする。クライアント側のRelatedLivesSectionは現状と同じく、受け取ったRelatedLiveGroup[]をそのまま描画するだけで変更しない(折りたたみ状態等のUI操作のみクライアント側の責務として残る)。

API失敗時の画面表示: 6-1と同様、専用のエラー表示は設けない。

並び順: グループ内のentriesは6-1と同じsortLivesForNavで日付昇順(同日は時刻昇順、00:00・未設定は末尾)に並べる。グループ自体は各グループの最初の公演日(firstDate)の降順(直近のツアー・フェスを先に表示)で並べる。いずれも比較は整形済み文字列ではなく元のDateオブジェクトで行い、その後日付を整形する。整形・並び替えはRoute Handler内で行い、クライアント側では行わない。

マッピングルール

フィールド 導出元
kind クエリ元(memberLiveなら'live'、memberFestivalなら'festival')
id live.liveId / festival.festivalId
date live.date / festival.dateを"YYYY/M/D"形式(月日は非ゼロパディング)に整形
time 6-1のtime導出ルールと同じ(ライブ・フェスともshouldShowTime・代表時刻のロジックを共有)
venueName venue.name
prefName venue.prefectureからprefecture_codes.nameを解決
isHometown member.profile.birthplaceとprefNameの一致判定。birthplace未設定時は全件false
groupKey・title・firstDate ツアー単位(live.tourId・tour.title)またはフェス単位(festival.name)でグルーピングし算出

7. 更新履歴

版 更新日 変更内容 関連 Issue
1.11 2026-09-22 §6を6-1(都道府県別公演画面、既存)・6-2(メンバー詳細画面の関連ライブセクション、新規)に分割。6-2に、新設エンドポイントGET /api/members/{id}/related-lives・RelatedLiveEntry/RelatedLiveGroupのZodスキーマ化・グルーピング/ご当地判定/時刻整形のRoute Handlerへの移植を追記。移植にあわせ、フェス出演のtime導出を現行実装(常にnull)から6-1と同じ代表時刻ロジックへ変更する仕様変更、およびグループの並び順(firstDate降順)を明記。6-1で保留していた「RelatedLiveEntryとの統合をあらためて検討する」の結論を「統合しない」と明記した MorningStatusApp#1654
1.10 2026-09-22 【対応する実装: 実装時に追記(MorningStatusApp#1545)】§2-7「festivals」に time(開催時刻)・performance_time(出演時刻)を追加し、時刻の運用ルール(NULL・00:00 は時刻不明、NULL 許容で追加する理由、タイムラインと一覧系の画面には、出演時刻を優先し不明なら開催時刻とする代表時刻を使う旨)を追記。§3のER図と§4の festivals.csv(時刻はシード用 CSV に含めない旨)を追随して更新。§6の FestivalFeedItem に代表時刻 time を追加し、マッピングルールを追記(都道府県別公演画面のフェス一覧にもライブと同じく時刻を表示するため) MorningStatusApp#1545
1.9 2026-09-20 §6の統合フィードアイテム データ設計書への参照章番号を、同設計書の章構成再編(適用判定ログ §6→§3、一般的な実装方法 §4・§5→§4)に追随して更新 MorningStatusApp#1632, morning-status-blume#138
1.8 2026-09-19 §6「BFF層設計(Route Handler + TanStack Query)」を新設。/lives/prefecture/[code]のみを対象にLiveFeedItem/FestivalFeedItem判別共用体・GET /api/lives・GET /api/festivalsを導入する設計を追記(他のライブ・フェス関連画面は単一形状・集計表示のため対象外と判定)。形状統合に該当する箇所(地図・会場数の集計)とLiveEventFeedItemの使いどころ、RelatedLiveEntryとの関係、prefecture不正時のAPI応答・API失敗時の画面表示、timeの整形方法も明記 MorningStatusApp#1636
1.7 2026-09-14 §2-10「member_festivals」を新設。member_livesと対になるメンバー×フェス出演紐づけの中間テーブルを追加し、member_livesの汎用化ではなく新規テーブル方式を採用した設計判断を明記 Tatsukiyoshi/morning-status-blume#120
1.6 2026-09-14 §2-4「member_lives」に「生成タイミング・書き込み経路」を新設。フェス・ツアー登録バッチ(O4)が公演登録時に在籍期間から対象メンバーを算出し書き込む方式を明記し、以前はメンバー詳細画面初回表示時の遅延初期化のみに依存し登録バッチ側で書き込んでいなかった不具合の経緯を備考として記載 MorningStatusApp #1619
1.5 2026-08-28 1曲が複数トラック(CD盤・配信版シングル等)に正しく該当しうるケースへの対応として、setlists.track_id 列を廃止し§2-9「setlist_tracks」中間テーブルに置き換え。あわせて release_id/旧 track_id の説明にあった「Blob管理のreleases.json参照」が実際はNeonへ移行済み(#854)であるという乖離を是正 MorningStatusApp #1529, morning-status-blume#80
1.4 2026-08-19 tour_id/festival_id のID採番ルール(季節コード対応表・スラッグ導出方針)を明文化 #1519
1.3 2026-05-09 setlists テーブル定義を実装仕様に更新(tour_id・festival_id FK、track_id 参照) #775
1.2 2026-05-08 festivals テーブル定義を追加 #776
1.1 2026-04-11 lives.time の 00:00 = 時刻不明仕様を明記 #528
1.0 2026-04-11 初版作成 #528

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