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

イベントテーブル定義書

最終更新: 2026-10-10 関連 Issue: #926(eventsテーブル実装), #989(カラム名修正), #997・#998(タイムラインUI表示仕様追加), #1070(imageUrl管理方針追加)【v5.0.0 タイムライン機能】 / #1108(V2設計書整備), #1109(festival統合), #1110(タグシステム)【v7.0.0 タイムライン機能拡張(V2)】 / #1381(イベントテーブル拡張・time カラム追加) / #1472(タイムライン改善: 同日順序・プレースホルダー全文表示・ツアー時刻二重表示解消) / #1551(TO_CHAR()日付比較クエリのunstable_cache化) / MorningStatusApp #1528(イベント登録における属性拡張), morning-status-blume#67(手動イベント登録の会場・楽曲属性設計。venueIdカラム・event_songs/event_song_tracksテーブル追加) / MorningStatusApp #1604・#1607(同期元テーブルのID変更時の孤立レコード対応) / MorningStatusApp #1545(フェスの時刻追加に伴う festival イベントの time 設定)

V1/V2 の定義: 「タイムライン機能」自体の拡張フェーズを表す通称であり、App本体の公式なバージョン体系(semver)そのものではない。

  • V1 = App v5.0.0 での実装を指す(#926・#989・#997・#998・#1070)
  • V2 = App v7.0.0 での実装を指す(#1108・#1109・#1110。タイムライン画面側の実装はタイムライン画面設計書を参照)

他の設計書で「V1」「V2」と表記する場合もこの定義に従う。


1. 目的

既存データ(Releases / Members / Lives / Radio / Manual Events)を時系列で横断的に扱うための統合イベントレイヤーを構築する。

  • 既存データは変更しない
  • events は「派生データ」として生成する
  • タイムライン UI は events のみを参照する

2. テーブル定義

テーブル名

events

スキーマ

カラム 型 NOT NULL デフォルト 説明
id UUID ✅ —(Prisma がアプリ層で UUID 生成) PK
date DATE ✅ — イベント発生日
time TIME(0) — NULL イベント開始時刻。lives.time と同じ表現方式(秒精度なし)。時刻不明・未登録時は NULL
type TEXT ✅ — イベント種別(後述)
title TEXT ✅ — イベントタイトル
description TEXT — NULL 補足説明
members TEXT[] — '{}' 関連メンバー ID 配列(members.json の id と対応)
tags TEXT[] — '{}' タグ配列(後述)
imageUrl TEXT — NULL 表示画像の URL(Vercel Blob URL 等)
link TEXT — NULL 関連リンク(MV / ニュース / radiko 等)
venueId VARCHAR(8) — NULL 会場ID(venues.venue_id 参照、FK制約あり)。手動イベント(source='manual')で venueName を指定・解決できた場合のみ設定する(§5-5参照)。live 型等、他の source では現状未使用(NULL のまま)。オンライン開催等、物理会場を持たないイベントもあるため任意項目
source TEXT ✅ — 元データ種別(後述)
sourceId TEXT ✅ — 元データの ID
createdAt TIMESTAMPTZ ✅ CURRENT_TIMESTAMP 作成日時
updatedAt TIMESTAMPTZ ✅ —(Prisma の @updatedAt で管理) 更新日時(更新時に自動更新)

2a. event_songs(イベント楽曲)

手動イベントの songs 入力(seq・songTitle)を保持する。radio_onair_songs・setlists と同じ「曲エンティティ自体のテーブル + トラック紐付け中間テーブル」の2階層構成を踏襲する(詳細はラジオオンエア データ設計書 §4-0参照)。イベント(Live・Festival)と同格に、曲情報を独立したレコードとして持たせる方針とする。

CREATE TABLE "event_songs" (
  "eventSongId" SERIAL        PRIMARY KEY,
  "eventId"     UUID          NOT NULL REFERENCES "events"("id") ON DELETE CASCADE,
  "seq"         INTEGER       NOT NULL,
  "songTitle"   TEXT          NOT NULL,
  "artistName"  VARCHAR(255),
  UNIQUE ("eventId", "seq")
);
カラム名 型 NOT NULL 説明
eventSongId SERIAL ✅ PK。radio_onair_songs.onairId と同じ代理キー方式(events.id が UUID のため、setlists.setlistId のような文字列組み立て PK ではなく独立した連番キーを持たせる)
eventId UUID ✅ FK → events(id)。イベント削除時に連動削除(CASCADE)
seq INTEGER ✅ イベント内での曲順(1始まり)。入力 JSON の seq をそのまま格納
songTitle TEXT ✅ 曲名。入力 JSON の songTitle をそのまま格納
artistName VARCHAR(255) — アーティスト名(他アーティスト楽曲向け、MorningStatusApp #1585)。radio_onair_songs.artistName と同じ拡張フィールド。NULL 許容のため既存データへの影響なし。詳細は §5-5「アーティスト指定(artist)」参照

インデックス:

CREATE INDEX "idx_event_songs_eventId" ON "event_songs" ("eventId");

備考:

  • (eventId, seq) の一意制約により、同一イベント内での seq 重複を防ぐ
  • トラックID への紐付けは本テーブルの列では持たず、§2b「event_song_tracks」の中間テーブルで管理する(1曲が複数トラック(CD盤・配信版シングル等)に正しく該当しうるため。ラジオオンエア・セットリストと同じ方針)
  • 曲名からトラックが1件も解決できなかった場合も、event_songs には曲名・seq が残る(event_song_tracks への行のみ作成されない)。これはラジオオンエア・セットリストの「不明な曲」パターンと同じ考え方であり、将来イベントタイムライン UI に曲情報を表示する際の受け皿になる。ただし本書の時点ではイベントタイムライン UI への曲情報表示自体は対象外(画面設計は別途検討)
  • artistName が設定されている曲(他アーティスト楽曲)は、trackId 解決自体を行わないため event_song_tracks への行は作成されない(「1件も解決できなかった」場合と異なり、これは意図した状態。詳細は §5-5 参照)
  • 本テーブルのカラム名は実際の DB カラム名(camelCase)で表記する。V2.5時点では便宜上 snake_case で記載していたが、実装(prisma/migrations/20260831071453_add_event_venue_songs)と乖離していたため是正した(events 本体テーブルの表記修正 #989 と同種の是正)

2b. event_song_tracks(イベント楽曲 × トラック紐付け)

CREATE TABLE "event_song_tracks" (
  "eventSongId" INTEGER       NOT NULL REFERENCES "event_songs"("eventSongId") ON DELETE CASCADE,
  "trackId"     VARCHAR(255)  NOT NULL REFERENCES "tracks"("trackId"),
  PRIMARY KEY ("eventSongId", "trackId")
);
CREATE INDEX "idx_event_song_tracks_trackId" ON "event_song_tracks" ("trackId");
カラム名 型 NOT NULL 説明
eventSongId INTEGER ✅ FK → event_songs(eventSongId)。複合 PK の一部。曲削除時に連動削除(CASCADE)
trackId VARCHAR(255) ✅ FK → tracks(trackId)。複合 PK の一部

セットリスト側にも同じパターンの中間テーブル(setlist_tracks)があり、詳細はライブ・フェス機能 テーブル設計書 §2-9を参照。


3. type(イベント種別)

値 説明 元データ V1/V2
release シングル・アルバム発売 releases V1
live ライブ・ツアー lives V1
join メンバー加入 members.json V1
graduate メンバー卒業 members.json V1
media ラジオ・TV 出演 radio_episodes V1
topic その他の話題・発表 manual V1
festival 外部フェス出演 festivals V2

festival type の仕様(V2)

  • festivals テーブルからの派生データ
  • source = 'festival'、sourceId = festival.festivalId
  • members フィールドは空配列(メンバー×フェス紐づけテーブルが存在しないため)
  • フェスが複数日にまたがる場合も dateEnd は持たない(セクション 3b 参照)。V2 時点では開始日のみ表示とし、期間表示が必要になった際に改めて検討する

3a. tags(タグ)仕様

tags フィールドは type とは別軸のイベント性質を表すラベル配列。フィルタリングやUIでの表示に使用する。

タグ値 説明 設定対象 type の例 付与方法
OG OG メンバーが関与するイベント 全 type 自動(assignTags)
Birthday 誕生日関連イベント 全 type 自動(assignTags)
Solo メンバーのソロ活動 topic, live, media 手動(manual-events.json)
Unit ユニット活動 release, live, topic 手動(manual-events.json)
Special 記念・特別イベント live, topic 手動(manual-events.json)
Collaboration 他アーティストとのコラボ release, topic 手動(manual-events.json)
  • タグは任意付与。付与なし(空配列)も有効
  • 1イベントに複数タグを付与可能
  • 自動付与タグ(OG・Birthday)は sync-events.ts の assignTags() 関数で付与される
    • OG: イベントに関与するメンバーのうち1名以上が status='OG'
    • Birthday: イベント日の UTC 月/日がいずれかの関与メンバーの profile.dob と一致
    • 全データソース(Releases / Members / Lives / Radio / Festivals / Manual)に適用される
  • data/inputs/manual-events.json の tags フィールドで任意タグを明示指定できる
    • tags を省略した場合 → assignTags() が OG・Birthday を自動付与
    • tags を明示指定した場合([] を含む)→ 明示値を優先し、assignTags() は適用されない

3b. dateEnd フィールドの要否

dateEnd(終了日)フィールドは当初 V2 設計として検討されたが、追加しないと判断した。

理由:

  • live 型はツアーの各公演を1レコードずつ持つ設計のため、期間を持つ概念がない
  • festival 型の元データ(festivals テーブル)は単日記録であり、終了日のデータソースがない
  • タイムライン UI での期間表示が必要になった時点で、データ供給方法と合わせて改めて検討する

4. source / source_id の意味

events は既存データの派生であるため、どのデータから生成されたかを保持する。

source 値 sourceId の意味 例
release Release.releaseId "release_001"
member Member.memberId(members.json の id) "nakazawa-yuko"
live Live.liveId "2026B001"
radio RadioEpisode.episodeId "morning-musume-radio-20240517"
manual 任意(手動入力時に付与) "topic-2024-01"
festival Festival.festivalId "summer-sonic-2025"

4a. 同期元テーブルのID変更時の孤立レコード対応

sourceId は元テーブルの主キー値のコピーであり、Prismaの正規のFK(@relation)ではないため、元テーブル側でID(Live.liveId・Release.releaseId等)が変更されても自動追従しない。

新IDのレコードは自動作成される

sync-events.ts は毎日定期実行され、source+sourceId で既存 Event レコードを検索し、無ければ新規作成する(upsertEvent())。そのため、元テーブルのID変更後に定期同期が1回動けば、新IDを指す正しい Event レコードは自動的に作成される。対応が必要なのは、使われなくなった旧ID側の孤立レコードの削除のみである。

削除対象は明示的なリストで指定する(自動検出はしない)

削除対象の旧ID一覧は、ID変更作業を行ったスクリプト・オペレーターが明示的に用意する。「元テーブルに存在しない sourceId を持つ Event を自動検出して削除する」という全ソース共通の突き合わせ処理は採用しない。

理由: member(Vercel Blob由来)・manual(manual-events.json由来)は、データ取得に失敗した場合に空配列へフォールバックするフェイルセーフ設計になっている(PR #1468 のインシデントを踏まえた意図的な設計)。この2つのsourceに対して「今回取得できなかった=元データから消えた」という自動突き合わせを行うと、Blob接続断や一時的なファイル破損だけで該当sourceの Event が全件誤削除される重大な事故につながる。live・release・radio・festival はDBテーブルを直接読むため理屈上は自動検出も安全だが、運用を複雑にしないため全sourceで明示リスト方式に統一する。

実装

scripts/console/ 配下に、source 種別と削除対象の旧 sourceId 一覧を受け取り、該当する Event レコードを削除するユーティリティスクリプトを用意する(--dry-run 対応、register-tour-lives.ts 等と同じ運用パターン)。

入力形式(孤立イベントリスト。単一カラムのリストのためCSVではなくプレーンテキスト。data/work/ 配下は.gitignore対象の一時データで、patch-birthplace.ts等の既存パッチスクリプトと同じ運用パターン):

live
2026C001
2026C002
  • 1行目: source 種別
  • 2行目以降: 削除対象の旧 sourceId(1行1件)

削除後の処理

  • 本実行(--dry-run なし)で Event を削除した後に、キャッシュを破棄する。孤立イベントリストを書き出す処理(リリース削除パッチなど)のキャッシュの破棄は、イベントの削除より前に行われ、削除前のイベントが画面に残るため。キャッシュの破棄に必要な環境変数(APP_URL・REVALIDATE_TOKEN)が無い場合は、削除の前にエラーで止める(--dry-run では不要)
  • 本実行(--dry-run なし)で Event の削除を終えたら、孤立イベントリストを削除する。処理済みのリストを残すと、次に孤立イベントリストを書き出す処理が、source の違いで止まるため
  • --dry-run と、削除が例外で失敗した場合は、削除しない。リストを残して、再実行できるようにする

孤立イベントリストの書き出し側の規則

孤立イベントリストを書き出す処理(リリース削除パッチ・手動登録リリースの洗い替え・重複リリースの統合パッチ)は、書き出し先の状態を確認し、DB の更新の前に判定する。DB を更新した後に判定すると、衝突で止まったときに、更新だけが済んだ状態になり、書き出すはずだった sourceId を失うため。

書き出し先の状態 動作
リストがない そのまま進む。DB の更新後に新規に書き出す
source=release の未処理リストがある そのまま進む。DB の更新後に、重複を除いて追記する
source=release 以外の未処理リストがある DB を更新する前にエラーで止める。メッセージに、書き出し先のパス、既存リストの source、「先に孤立イベントレコード削除ユーティリティで処理してから再実行する」手順を含める
  • エラーで止めた時点では DB を更新していないため、リストを処理した後に、同じコマンドをそのまま再実行できる。自動では処理しない
  • --dry-run でも同じ確認をして、同じエラーで止める。確認は、削除計画などの出力より前に行う
  • リストを書き出す必要がある場合だけ確認する(リリース削除パッチは削除するリリースを元にしたイベントがある場合、手動登録リリースの洗い替えは書き出す旧 releaseId がある場合、重複リリースの統合パッチは常に)

5. 変換ロジック仕様

変換レイヤーの実装は #927 で行う。本書には入力元フィールドのマッピング仕様のみを記載する。

5-1. Releases → events(release)

events カラム 元フィールド
date release.releaseDate
type "release"
title release.title
members releases.json のメンバー紐づけ
imageUrl Cover Art Archive から取得したジャケット画像 URL(後述)
link releases.json の MV URL
source "release"
sourceId release.releaseId(= MusicBrainz リリースグループ MBID)

5-2. Members → events(join / graduate)

加入(join)

events カラム 元フィールド
date member.joined(members.json)
type "join"
title "{期}期加入"
members [member.id]
imageUrl member.image(members.json)
source "member"
sourceId member.id

卒業(graduate)

events カラム 元フィールド
date member.graduated(members.json)
type "graduate"
title "{name} 卒業"
members [member.id]
imageUrl member.image(members.json)
source "member"
sourceId member.id

5-3. Lives → events(live)

events カラム 元フィールド
date live.date
time live.time(そのままコピー)
type "live"
title live.title(または tour.title)
members MemberLive 経由の memberId 配列
source "live"
sourceId live.liveId

5-4. Radio → events(media)

events カラム 元フィールド
date radioEpisode.date
type "media"
title radioShow.name
members RadioMember 経由の memberId 配列
source "radio"
sourceId radioEpisode.episodeId

5-5. Manual → events(topic)

全フィールド手動入力。source = "manual"、source_id は任意の識別子を付与する。

data/inputs/manual-events.json の time フィールド("HH:MM" 形式の文字列、任意)を指定すると、time カラムに変換される。省略時は既存値を保持する(imageUrl と同じ undefined/null の使い分け。#1381)。

会場(venueName → venueId)

data/inputs/manual-events.json に任意項目 venueName(会場名の文字列)を指定すると、events.venueId に変換される。物理会場を持たないイベント(オンライン開催等)も扱えるよう venueName は省略可能とする。venueName を省略した場合、既存の venueId を保持する(time と同じ undefined 時保持パターン)。

解決手順は register-tour-festival.ts(ライブ・フェス機能 テーブル設計書)の会場登録ロジックをそのまま流用する。

  1. 既存 venues.name と完全一致する会場があれば、その venueId を再利用する
  2. 一致しない場合、Google Maps Geocoding API(環境変数 GEOCODING_API_KEY)で venueName をそのままクエリとして緯度経度・都道府県名を取得する(region=jp に加え language=ja を指定し、都道府県名の表記言語を日本語に固定する。#1520)
  3. 都道府県コードへの変換に失敗した場合、または API 呼び出し自体が失敗した場合は、当該会場名を警告ログに出力し、そのイベントの venueId は更新しない(既存値があればそれを保持、なければ NULL のまま。他のイベントの処理は継続する。グレースフルな失敗処理)
  4. 新規解決できた会場は都道府県コード + 連番3桁で venueId を新規採番し、venues へ登録する

GEOCODING_API_KEY が未設定の場合、会場解決自体をスキップする(venueName 指定イベントは venueId を更新せず同期される。Releases / Lives / Radio 等、会場を伴わない他のイベント同期には影響しない)。

本仕様は会場の「登録・変更」のみを扱う。venueName に明示的な「未設定に戻す」値(null 等)は用意しない。一度 venueId が設定されたイベントについて、venueName を省略しても venueId は変更されない(前述の undefined 時保持)ため、会場を後から解除したい場合は本仕様の対象外(必要になった時点で別途検討する)。

楽曲(songs → event_songs / event_song_tracks)

data/inputs/manual-events.json に任意項目 songs(SongEntrySchema を拡張したスキーマ。scripts/lib/song-entry-schema.tsの基底seq・songTitleに加え、任意項目artist(他アーティスト楽曲向け、MorningStatusApp #1585)を持つ。基底スキーマ自体はラジオオンエア・セットリストの入力と共通。詳細はラジオオンエア データ設計書 §4-0参照。artist拡張の仕様は下記「アーティスト指定(artist)」参照)を指定すると、§2a「event_songs」・§2b「event_song_tracks」に変換される。

同期は seq をキーにした差分方式。 events 本体が source+sourceId をキーに差分upsert(upsertEvent()。既存レコードは id を保持したままUPDATE、新規のみINSERT)する既存設計と対称に揃える。event_songs.eventId は events(id) への FK のため、event_songs/event_song_tracks への書き込みは upsertEvent() で対象イベントの id が確定した後に行う(新規の手動イベントは events 行の INSERT より前に曲を書き込むことはできない。処理順序の詳細はバッチ設計書 W6参照)。songs を省略した場合、既存の event_songs/event_song_tracks を保持する(変更しない)。songs を明示指定した場合([] を含む)は、そのイベントの既存 event_songs(seq で識別)と入力 songs を突き合わせ、以下のように処理を分ける。

ケース 処理
既存に同じ seq があり songTitle・artistName も同じ event_songs 行は変更しない(eventSongId を保持)
既存に同じ seq があるが songTitle または artistName が異なる 該当行の songTitle・artistName を UPDATE(eventSongId は保持)
既存にない seq 新規 INSERT(新しい eventSongId を採番)
入力 songs に含まれない既存 seq 該当行を DELETE(event_song_tracks は CASCADE で連動削除)
  • 本仕様は「楽曲の登録・追加・削除・全削除」のいずれも扱う。 入力 songs 配列に曲を足せば追加、配列から曲を除けば削除、配列を [] にすれば全削除になる
  • eventSongId は、対応する seq の曲が存在し続ける限り再同期をまたいで保持される。内容に変更がない曲について event_songs 行を毎回 delete→re-create することはしない
  • 一方 event_song_tracks(トラック紐付け)は、songs に含まれる全曲のうち artist 未指定・空文字の曲について毎回 buildTrackCandidatesMapFromPrisma で再解決し、該当 eventSongId の既存リンクを削除してから書き直す。event_song_tracks は複合PK(eventSongId・trackId)のみで独自の代理キーを持たないため、event_songs と異なり洗い替えても ID の再採番は発生しない。この再解決を毎回行う理由は、曲名からtrackIdが1件も解決できなかった曲(発表当初はTrack未登録の新曲等)が、後日 Track が正式登録された時点で自動的に紐付くようにするため(追加の再解決バッチを別途用意せずに済む)

アーティスト指定(artist)

songs[].artist を指定すると、その曲を「モーニング娘。関連ユニット以外のアーティストの楽曲」として扱う(他アーティストとのコラボ楽曲・カバー等)。

artist の値 扱い
省略、または空文字 "" 従来通り、モーニング娘。関連ユニットの楽曲としてタイトルから trackId を解決する。artistName は NULL
空でない文字列 trackId 解決を行わず、指定値をそのまま event_songs.artistName に格納する(event_song_tracks への行は作成しない)

ラジオオンエア(RadioOnairSong.artistName。register-manual-radio-episodes.tsのresolveSong)と同じ「artistNameが設定されている曲はtrackId解決を行わない」という考え方を踏襲するが、スキーマの拡張方法はラジオ側と異なる。ラジオ側はSongEntrySchema.extend({ artist: z.string() })(artistキー必須、空文字がown-group扱いの表現)である一方、events側はSongEntrySchema.extend({ artist: z.string().optional() })(artistキー自体を省略可能)を採用する。manual-events.jsonのsongsはモーニング娘。関連楽曲が大半を占め、他アーティスト楽曲は例外的なケースであるため、通常の曲すべてに"artist": ""を明記させるより省略を許容する方が入力の手間が小さい。

artist指定曲はtrackIdが0件になるが、これは意図した状態であり、後述の「該当トラックが1件も見つからない曲」向けの警告ログ(convertManualEventsの「楽曲未登録の可能性」ログ)の対象にはしない。

  1. songs 内の各曲について、artist が指定されている(空文字でない)場合は trackId 解決をスキップする。それ以外(省略または空文字)の場合は、従来通り lib/track-match.ts の buildTrackCandidatesMapFromPrisma で曲名から該当する全 trackId 候補を解決する(1曲が複数トラック(CD盤・配信版シングル等)に正しく該当しうるケースへの対応。ラジオオンエア・セットリストと同じロジックを再利用する)
  2. 上記の差分ルールに従って event_songs を INSERT/UPDATE/DELETE する(songTitle・artistName を保存)
  3. artist 未指定・空文字の曲(eventSongId が確定したもの全て)について、既存の event_song_tracks を削除し、解決できた trackId を全件登録し直す(lib/track-match.ts の buildTrackJunctionRows を再利用)。artist 指定曲は event_song_tracks への登録を行わない
  4. artist 未指定・空文字の曲のうち、該当トラックが1件も見つからないものは、event_songs には登録するが(曲名・seq は残る)、event_song_tracks への登録は行わない。該当曲名を警告ログに出力する(register-tour-festival.ts の unresolvedSongTitles と同じ扱い)

5-6. Festivals → events(festival)(V2)

events カラム 元フィールド
date festival.date
time 出演時刻(festival.performanceTime)が有効(NULL・00:00 以外)ならその値。それ以外は開催時刻(festival.time)をそのまま使う(00:00 もそのまま入り、NULL の場合は NULL)。時刻の表示可否は §8-3 の EventCard の判定(00:00・NULL は非表示)に従う
type "festival"
title festival.name
members [](メンバー×フェス紐づけなし)
source "festival"
sourceId festival.festivalId
  • タイムラインにはモーニング娘。が出演する時刻を載せたいため、出演時刻を優先し、不明な場合に限って開催時刻(フェス全体の開演時刻)を使う。時刻の意味と運用ルールはライブ・フェス機能 テーブル設計書 §2-7を参照
  • 元データ(festivals)の時刻を後から登録・訂正した場合は、次回のイベント同期バッチ(バッチ設計書 W6)の実行で、既存の events レコードの time にも反映される
  • 時刻の表示は §8-3 の EventCard の時刻表示に従う(フェス由来のイベントも対象)

6. imageUrl 管理方針

6-1. release イベントのジャケット画像(#1070)

source = 'release' のイベントは Cover Art Archive(CAA) からジャケット画像を取得して imageUrl に保存する。

データフロー

releases.releaseId
  = MusicBrainz リリースグループ MBID
    └─ https://coverartarchive.org/release-group/{MBID}/front
         → 307 リダイレクト → archive.org 上の実画像 URL
              → events.imageUrl に保存

取得タイミング

imageUrl の自動設定は行わず、以下のタイミングで手動実行する。

タイミング 手順
初回一括バックフィル bun run scripts/patch/fetch-release-images.ts を実行
新シングル発売後 MusicBrainz 登録・sync-discography・sync-events 完了後に同スクリプトを実行

詳細な手順は docs/operations/discography-operations.md の「新シングル発売時のカバーアート取得手順」を参照。

取得対象外

  • source = 'manual'(manual- プレフィックス ID)の手動登録リリース→ MusicBrainz MBID を持たないため CAA 取得不可

6-2. release 以外のイベントの画像表示(#1070、プレースホルダー方式見直し #1472)

imageUrl = NULL かつ type ≠ release のイベントは、EventCard コンポーネントが種別に応じたプレースホルダーを動的生成する。DB への保存は不要。プレースホルダーは表示内容によって2方式に分かれる。

方式A: SVG テキストアート(live・join・graduate・leader)

タイトル文字列そのものではなく、種別ごとに合成した短い定型テキスト(開催年・JOIN/GRAD ラベル・メンバー名等)を固定サイズの viewBox に描画する。表示内容が短く定型のため、折り返しや省略は発生しない。

type SVG に表示する内容
live 開催年 + 季節(UTC 月から算出: 3〜5月→春、6〜8月→夏、9〜11月→秋、12〜2月→冬)
join 1行目: JOIN / 2行目: メンバー名(1行に収まる範囲、超過は省略)
graduate 1行目: GRAD / 2行目: メンバー名(1行に収まる範囲、超過は省略)
leader 1行目: 第X代 / 2行目: リーダー就任

方式B: HTML テキストブロック(media・topic・festival、#1472)

events.title をそのまま全文表示する種別。SVG の <text> は CSS の折り返し(white-space/overflow-wrap)に対応しておらず、行数を固定して溢れた分を … で省略する方式だと、タイトル長が可変な限り必ず一部のケースで全文を表示できなくなる問題があった(#1472で発覚)。そのため、これらの type は <div> 等の HTML ブロック(overflow-wrap: break-word)で描画し、コンテナは固定の縦横比をやめて min-height のみを指定し、コンテンツ量に応じて高さが伸びる可変レイアウトにする。行数の上限は設けず、events.title を省略なく全文表示する。

type HTML ブロックに表示する内容
media 番組名(events.title、全文)
topic タイトル(events.title、全文)
festival フェス名(events.title、全文)

6-3. タイトル表示の重複回避(#1472)

EventCard は画像・プレースホルダー領域の下に <h3> でタイトルを別途表示するが、6-2 の方式Bはプレースホルダー自体に events.title の全文を表示するため、imageUrl 未設定時は <h3> と内容が完全に重複する。

  • imageUrl = NULL かつ 6-2 方式B対象の type(media・topic・festival)の場合のみ、<h3> タイトルを非表示にする
  • 上記以外(imageUrl が設定されている場合、または方式A対象の type)は、プレースホルダー側がタイトル文字列自体を表示しないため、従来通り <h3> を表示する

7. インデックス

インデックス対象 インデックス種別 理由
date B-tree タイムライン表示(ORDER BY date)の主要クエリキー
type B-tree イベント種別フィルタリング
source, sourceId B-tree 元データからのルックアップ・重複排除
tags GIN タグ配列フィルタリング(@> ARRAY[...])(フィルタ機能実装時に追加予定)

7-1. TO_CHAR(date, ...) による日付比較クエリのキャッシュ方針(#1551)

「過去のこの日」(トップページ)・タイムラインの年一覧取得は、date の月日・年だけを取り出して比較するため TO_CHAR() を使った生SQLクエリになっている。TO_CHAR() は date カラムに関数をかけてから比較する形になり、7. の date への B-tree インデックスが使えず全件スキャンになる。

関数 用途 クエリ内容
lib/on-this-day.ts getOnThisDayEvents(monthDay) トップページ「過去のこの日」セクション WHERE TO_CHAR(date, 'MM-DD') = ${monthDay}
lib/timeline.ts getTimelineYears() タイムラインページの年タブ一覧 SELECT DISTINCT TO_CHAR(date, 'YYYY') AS year FROM events

いずれも unstable_cache(revalidate: 3600秒)でラップし、DBへのクエリ発行自体を最大1時間に1回へ抑える(lib/youtube.ts と同パターン)。全件スキャンそのものを解消する関数インデックスの追加は本番DBへのマイグレーションを伴うため別Issue(#1554)で扱う。

getOnThisDayEvents のキャッシュには on-this-day タグを付け、フェス詳細画面(デスクトップモード)の編集の保存が成功したとき(ライブ・フェス機能 画面設計書 §10)に、このタグのキャッシュを破棄する(MorningStatusApp#1545)。過去のこの日は、フェス出来事の会場名と時刻を festivals から引いて組み立てるため、保存した会場・日付・時刻が、最大1時間の待ちなしで反映される。ライブの編集(PATCH /api/lives/[id])ではこのキャッシュを破棄しないため、ライブ出来事の会場名・時刻の変更は、最大1時間遅れて反映される(フェスの時刻追加(MorningStatusApp#1545)のスコープに限定してキャッシュ破棄を実装したため。ライブ側への同様の対応は本Issueの対象外)。

備考: 以前は getOnThisDayEvents() にキャッシュがなく、アクセスの度にNeon DBへ生クエリを発行していた。同じトップページ内の「新着投稿」セクション(キャッシュ経由でほぼ即時に返る)とは別々の Suspense で並行ストリーミングされるため、データ取得が先に完了した方から画面に反映される仕様と相まって、app/page.tsx 上でのセクション配置順(過去のこの日が先)と実際の表示順が逆転する不具合が発生していた(#1551で修正)。


8. タイムライン UI 表示仕様

8-1. 年別表示(#998)

  • タイムラインページは 1年分のイベントのみ を表示する
  • URL クエリパラメータ year(例: /timeline?year=2024)で表示年を指定する
  • 未指定・無効値の場合は最新年を表示する
  • ページ上部にすべての年のタブリンクを表示し、選択中の年をハイライトする

8-2. ライブイベントのグループ表示(#997)

同一ツアー(title が同じ type=live イベント群)は1グループとして折りたたんで表示する。

グループの公演数 表示方法
1件 通常のフルサイズカード(会場名を日付の後に表示)
2件 2枚のフルサイズカードを並列表示(会場名あり)
3件以上 初日・千秋楽をフルサイズカード、中間日をコンパクトリスト(日付 / 会場 / 開演時刻)で表示
  • 会場名・開演時刻は lives テーブルを sourceId(liveId)経由でジョインして取得する
  • 同日に複数公演がある場合は1行にまとめ、時刻を "14:00 / 18:00" 形式で列挙する
  • 開演時刻が 00:00(未設定)の場合は時刻を表示しない

8-3. EventCard の時刻表示(#1381)

ツアーグループ化されない単発イベント(EventCard で直接描画される全 type)は、events.time を日付の直後に表示する。

  • time が NULL または 00:00 の場合は表示しない(8-2 の Live 表示ルールと同じ判定関数 shouldShowTime/formatLiveTime を共通利用)
  • type=live のツアーグループ表示(8-2)は lives テーブルの時刻を別途取得して表示するため、events.time の表示とは独立している
  • 8-2 のツアーグループで会場欄に複数公演の時刻一覧("14:00 / 18:00")が含まれる代表カード(初日・千秋楽、または同日2件時の1枚カード)は、時刻が会場欄と二重表示にならないよう events.time 自体の表示を抑制する(#1472)

8-4. イベントの並び順(#1472)

タイムライン表示(年・週・日・年表の各ビュー)で events を取得するクエリは、date 昇順に加えて time を副次ソートキー として指定する(orderBy: [{ date: "asc" }, { time: "asc" }])。

  • date のみで ORDER BY すると、同日内のタイ(同値)の並び順は Postgres が保証しないため、物理的な行順(挿入順等)が表示順になり不安定になる(#1472で発覚)
  • time が NULL のイベントは Postgres の ASC デフォルト挙動(NULLS LAST)により末尾に並ぶ

改訂履歴

版 更新日 変更内容
2.11 2026-10-10 【実装時に追記(MorningStatusApp#1739)】§4a「実装」に、「削除後の処理」(本実行でイベントを削除した後にキャッシュを破棄し、削除を終えたらリストを削除する)と「孤立イベントリストの書き出し側の規則」(書き出し先に source=release 以外の未処理リストがある場合は、DB の更新の前にエラーで止める)を追記。削除用リストの呼び名を「孤立イベントリスト」に統一(MorningStatusApp#1739)
2.10 2026-09-27 §N参照をセクション直リンクに強化([#custom-id]アンカー導入)。あわせて、V1/V2の定義を冒頭に明記し、§3・§5-6のfestivalの「V2追加予定」という記載(実装は#1109で2026-06-14に完了済だったが記載が追随していなかった)を「V2」に是正(morning-status-blume#108)
2.9 2026-09-22 【v10.6.2(MorningStatusApp#1545)】§5-6「Festivals → events(festival)」に time の変換ルールを追加(出演時刻を優先し、不明な場合は開催時刻)。時刻を後から登録・訂正した場合の反映タイミングと、時刻の表示(§8-3)が対象になる旨を追記。§7-1 に、フェス詳細画面の編集の保存が成功したときに、過去のこの日のキャッシュ(on-this-day)を破棄する旨を追記(MorningStatusApp#1545)
2.8 2026-09-09 H1タイトルを日本語表記に統一(events テーブル定義書 → イベントテーブル定義書)。エンティティ名の日本語表記規約に合わせ、他設計書(data-design.md・batch-design.mdx・event-screen-design.md・release-data-design.md)からの内部リンクのリンクテキストも追随修正(morning-status-blume#113レビューで検出)
2.7 2026-09-07 §4a「同期元テーブルのID変更時の孤立レコード対応」を新設。sourceId が元テーブルの正規のFKでないため元テーブル側のID変更に自動追従しない問題(liveのツアーID採番ミス訂正で発覚、29件の孤立Eventレコードが残存)への対応方針を追記。新IDのレコードはsync-events.tsの定期同期が自動作成するため、削除対象の旧sourceIdを明示リストで受け取り該当Eventを削除するユーティリティ(data/work/event-source-ids-to-delete.txt)を追加する設計とした(MorningStatusApp #1604, #1607)
2.6 2026-09-05 §2a event_songs に artistName カラムを追加。§5-5 に「アーティスト指定(artist)」を新設し、他アーティスト楽曲の trackId 解決スキップ・event_song_tracks 非登録の仕様を追記(events用スキーマ拡張はラジオ側と異なりartistキー省略可とした理由も明記)。あわせて §2a・§2b の SQL 定義・カラム名表記を、V2.5時点の誤った snake_case から実際の DB カラム名(camelCase)に是正(実装は別Issueで対応。MorningStatusApp #1585)
2.5 2026-08-30 手動イベント登録に「会場」「楽曲」属性を追加する設計を追記。events.venueId カラム、event_songs・event_song_tracks テーブル(§2a・§2b)を新設。§5-5 に venueName → venueId の解決手順(register-tour-festival.ts の会場登録ロジックを流用)と songs → event_songs/event_song_tracks の変換手順(ラジオオンエア・セットリストと共通の SongEntrySchema・buildTrackCandidatesMapFromPrisma を再利用)を追加。実装は別Issueで対応(MorningStatusApp #1528, morning-status-blume#67)
2.4 2026-08-23 7-1. TO_CHAR(date, ...) を使う日付比較クエリ(getOnThisDayEvents・getTimelineYears)の unstable_cache 化を追記。全件スキャン解消(関数インデックス追加)は別Issue(#1554)に切り出したことを明記(#1551)
2.3 2026-08-03 タイムライン改善(#1472): 同日イベントの並び順仕様(8-4)を追加、ツアーグループ代表カードの時刻二重表示回避仕様(8-3)を追加、media/topic/festival のプレースホルダーをSVG2行折り返しからHTML全文表示(可変高さ)に変更し6-2を更新、タイトル重複回避仕様(6-3)を追加
2.2 2026-07-20 time カラムを追加。Lives → events(live) の変換仕様・Manual の time 入力仕様・EventCard の時刻表示仕様(8-3)を追記(#1381)
2.1 2026-06-14 tags フィールドを実装済に更新。OG・Birthday 自動タグ仕様を追記。手動タグ付与フローを追記(#1110)
2.0 2026-06-13 V2 拡張設計を追記(festival type・tags フィールド・dateEnd フィールド・GIN インデックス)。セクション番号重複を修正(#1108)
1.3 2026-06-05 imageUrl 管理方針を追加(CAA によるジャケット画像取得・SVG テキストアート)(#1070)
1.2 2026-05-29 タイムライン UI 表示仕様を追加(年別表示・ライブグループ表示)(#997, #998)
1.1 2026-05-24 カラム名を実際の DB カラム名(camelCase)に修正。id・updatedAt のデフォルト値記述を修正(#989)
1.0 2026-05-24 初版作成(#926)

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