ライブ・フェス機能 テーブル設計書
最終更新: 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 |