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

イベント詳細画面 設計書

最終更新: 2026-09-09 関連 Issue: morning-status-blume#97(本設計書作成)、morning-status-blume#101(「戻る」ナビゲーション設計の誤り修正) 親 Issue: MorningStatusApp#1530「イベント・トピックの表示」 依存: MorningStatusApp#1533(手動イベント登録の会場・楽曲属性実装。events.venueId・event_songs・event_song_tracks。クローズ済)


1. 概要

events(イベントテーブル定義書)のうち source = 'manual'(手動登録イベント) を対象に、単独の詳細画面と、メンバー詳細画面からの導線を追加する。

対象範囲は type ではなく source で決める

data/inputs/manual-events.json で手動登録されるイベントは type が topic(現状59件)だけでなく media(現状14件、例: music-station-1998-01-30「ミュージックステーション出演」)でも登録されている。これは、モーニング娘。メンバーが音楽番組・ラジオ番組(追跡対象外の番組)・Podcast・バラエティ等に単発でゲスト出演した記録を、専用データモデルを持たないため manual-events.json 経由で登録しているケースである。

この source='manual' かつ type='media' のイベントは、sourceId が radio_episodes.episodeId に対応しないため、RadioAppearancesSection(RadioMember/RadioEpisode テーブル経由でのみ取得。§3参照)には一切表示されない。typeだけを基準に対象を決めると、この手動登録mediaイベントが既存のどの画面にも表示されない「一覧漏れ」を見落とす。 正しい基準は「他のいずれの自動同期パイプライン・専用画面にも属さないイベント」であり、これは events.source = 'manual' と同値になる。

source 詳細画面を新設するか 理由
manual(手動登録。実データ上 type は topic または media) ✅ 本設計書§2で新設(/events/[id]) 自動同期パイプラインを持たず、専用の詳細画面がどこにも存在しない
release ❌ /releases/[id] が既存
live ❌ /lives/[id] が既存
festival ❌ /festivals/[id] が既存
radio(type=mediaの自動同期分。radio_episodes由来) ❌ /radio/episodes/[episode_id] が既存
member(typeがjoin/graduate) ❌ メンバープロフィールヘッダに加入日・卒業日が既に表示されている

変更点サマリー

  1. 新規画面: イベント詳細画面(/events/[id]、source='manual'のイベントが対象)── §2
  2. 既存画面への追加: メンバー詳細画面(/members/[id])に「イベント一覧」セクションを追加(source='manual'のイベントが対象)── §3

2. イベント詳細画面(/events/[id])

概要

source = 'manual' のイベント1件の詳細(日時・説明・会場・関連メンバー・楽曲情報)を表示する画面。/lives/[id]・/festivals/[id] と役割・表示項目の構成は同等とするが、編集機能は持たない(後述)。

ファイル構成

app/events/[id]/page.tsx(Server Component、export const dynamic = 'force-dynamic')1ファイルにJSXを直接記述する。LiveDetailView/FestivalDetailViewのような別ファイルへの分離は行わない。

  • Live/Festival詳細が 'use client' の *DetailView コンポーネントに分離されているのは、デスクトップモードの会場・日時編集UI(useState・PATCH API呼び出し)を持つため。本画面はその種の編集機能を持たないため、app/radio/episodes/[episode_id]/page.tsx と同じ「Server Componentのみ」のシンプルな構成を採用する
  • 「前の画面に戻る」(後述)はブラウザ履歴を使うクライアント側処理が必要になるが、これは既存のBackLinkコンポーネント('use client' の独立した leaf コンポーネント。曲詳細画面・リリース詳細画面・対比詳細画面で既に採用されているパターン)をそのまま埋め込むだけで実現できる。ページ全体を 'use client' にしたり、Live/Festivalのような編集状態を持つ *DetailView へ分離したりする必要はない

データ取得

const event = await prisma.event.findUnique({
  where: { id },
  include: {
    venue: true,
    songs: {
      orderBy: { seq: 'asc' },
      include: { tracks: { select: { trackId: true } } },
    },
  },
});

アクセス制御

条件 挙動
[id] に一致する events レコードが存在しない notFound()
レコードは存在するが source !== 'manual' notFound()(§1の表の通り、manual以外のsourceは専用の詳細画面が別URLに存在するため、このURLでは扱わない。誤って直リンクされた場合は404にする)

表示コンテンツ

要素 内容
ナビゲーション 「前の画面に戻る」ボタン(components/BackLink.tsx を使用。ブラウザ履歴があれば直前の画面へ戻り、履歴がない場合(直リンクアクセス等)は /timeline へフォールバック)
タイトル events.title
日付 events.date(ライブ・フェス機能 画面設計書のライブ詳細と同じformatLiveDateDetailを流用。例: 2026年4月11日(土))
時刻 events.time が設定されている場合のみ表示(shouldShowTime/formatLiveTime をそのまま流用。例: 19:00)
説明文 events.description が設定されている場合のみ表示
会場 events.venueId が設定されている場合のみ表示。venues.name + 都道府県名(prefectureCode テーブルから変換。ライブ・フェス詳細と同じ prefName 解決パターン)。未設定(オンライン開催等、会場を持たないイベント)の場合は行自体を非表示にする
関連メンバー 後述
楽曲情報 後述

前後ナビゲーション: 設けない。ライブ詳細の「前の公演/次の公演」はツアー内の公演という自然な順序集合があるために成立するが、手動登録イベント同士にはそのような単位がなく、また呼び出し元(タイムライン・メンバー詳細)によって「前後」の文脈も変わるため、単純化して省略する。

関連メンバー欄

  • データソース: events.members(String[])。getMembersFromBlob() で全メンバーを取得し、event.members に含まれる id でフィルタする
  • 動的補完は行わない: source='manual' のイベントは sync-events.ts の ManualEventSchema により常に members が明示的に設定される(manual-events.json の members は必須項目で省略不可)。lib/timeline-utils.ts の withResolvedMembers/resolveEventMembers による在籍期間ベースの補完は live・festival(TENURE_FALLBACK_TYPES)専用のフォールバックであり、source='manual' には適用されない・する必要もない
  • ソート: 世代昇順・加入日昇順(/radio/episodes/[episode_id] の出演メンバーソートと同じ)
  • 表示: メンバー名+{generation}期バッジのチップ、/members/{id} へリンク(LiveDetailView の「出演メンバー」欄と同じスタイル)
  • ゼロ件時: セクション自体を非表示(実データでも members: [] の手動イベントは多数存在する。例: §1で挙げたmusic-station-1998-01-30)

楽曲情報欄

  • データソース: event.songs(EventSong[]、seq昇順)。各曲の tracks(EventSongTrack[])から trackId を解決する
  • 表示パターン: app/radio/episodes/[episode_id]/page.tsx(127-149行目)のオンエアリスト表示と同じ構造を流用する
    • tracks.length > 0 の曲: 曲順(seq)・曲名・trackId ごとに /songs/{trackId} への♪リンク(1曲が複数トラックに該当する場合は複数表示)
    • tracks.length === 0 の曲(曲名からトラックを解決できなかった曲): 曲順・曲名のみ表示(リンクなし)
    • 備考: radio_onair_songs と異なり event_songs にはアーティスト名列がないため、放送回詳細のような「/ アーティスト名」の併記は行わない
  • ゼロ件時: セクション自体を非表示にする(event_songs はイベントごとに登録の有無が分かれる任意データであり、「オンエアリスト」のような番組の必須要素ではないため、「データがありません」という空表示ではなく、RelatedLivesSection 等の他の任意セクションと同じくセクション自体を非表示にする方針を採る)

タイムラインからの導線

EventCard(components/EventCard.tsx)に、source='manual' のイベント(topic・mediaとも)向けの本画面(/events/[id])への遷移リンクを追加する。

  • EventCardEvent型にsource?: stringを追加: app/timeline/page.tsxのrenderEventCardは、DB取得済のEventRow(source・sourceIdを含む)をEventCardのevent propにスプレッドで渡しており、実行時にはsourceの値は既に渡っている。ただしEventCardEvent型(components/EventCard.tsx)にsourceフィールドの宣言がないため型的に参照できない。型定義にsource?: stringを追加するだけでよく、DB取得ロジックの変更は不要
  • 表示条件: event.source === 'manual'の場合のみ詳細リンクを表示する(radio由来のtype='media'イベントは/radio/episodes/[episode_id]が既存のため対象外。§1の対象範囲表と同じ判定基準)
  • 表示位置・スタイル: .content内、既存の外部リンク(event.link、styles.link)と同じ並びに「→ 詳細を見る」テキストリンクを追加する。next/linkのLink href={/events/${event.id}}を使用し、既存のstyles.linkと同等のスタイル(display: inline-flex・color: var(--primary-color))を流用する
  • 本導線の追加により、本画面への遷移元が「メンバー詳細画面」(§3)と「タイムライン画面」の2つになる。「前の画面に戻る」(前述)は、この複数遷移元に対応するための変更である

備考: 旧仕様からの変更経緯: 初版(1.0)では「戻る」の行き先を/timelineに固定していた。当時、本画面への実際の遷移元は「メンバー詳細画面 → イベント一覧」(§3)のみだったにもかかわらず、検証を経ずに「タイムラインに戻る」と書いてしまった誤りであり、メンバー詳細画面経由で来たユーザーが「戻る」を押すと、一度も経由していないタイムライン画面に飛ばされてしまう不具合があった(morning-status-blume#101)。本改訂でタイムラインからの導線を追加し遷移元が複数になったことを機に、固定リンクではなくブラウザ履歴に基づく「前の画面に戻る」に修正した。


3. 既存画面への追加:メンバー詳細画面(/members/[id])「イベント一覧」セクション

概要

source = 'manual' のイベントのうち、当該メンバーが関連するものを一覧表示するセクション(EventsSection)を追加する。

source 対象外とする理由
release 既存の専用セクション(RelatedReleasesSection)が既に一覧表示しているため
live 既存の専用セクション(RelatedLivesSection)が既に一覧表示しているため
radio(type=mediaの自動同期分) 既存の専用セクション(RadioAppearancesSection)が既に一覧表示しているため
member(typeがjoin/graduate) プロフィールヘッダに既に加入日・卒業日として表示済のため
festival Event.members が常に空配列(メンバー×フェス紐づけデータが存在しないため。イベントテーブル定義書 §3)で、そもそも本人に紐づくレコードが取得できない

manual(実データ上 type は topic または media)だけが上記いずれの既存表示にも属さない。§1の通り、radio_episodesに対応しない手動登録のmediaイベント(音楽番組・バラエティ等への単発ゲスト出演)もここに含まれる。

メンバー×イベントの紐付けクエリ方針

events.members は String[] のスカラー配列カラムで、member_lives のような中間テーブルは存在しない。メンバー詳細画面で「このメンバーに関連するイベント」を取得するクエリは以下の通り。

const memberEvents = await prisma.event.findMany({
  where: {
    source: 'manual',
    members: { has: id },
  },
  orderBy: [{ date: 'desc' }, { time: 'desc' }],
  include: { venue: true },
});
  • typeではなくsource: 'manual'でフィルタする: §1の通り、対象を決める基準はtypeの列挙ではなくsource。typeで列挙する実装(例: type: { in: ['topic', 'media'] })にすると、将来manual-events.jsonで他のtypeが手動登録された場合に追随できず、かつradio_episodes由来の自動同期media(source='radio')との区別をtypeだけでは表現できない
  • has(単一値の包含判定)を使う。hasSome(複数値のいずれかとの重複判定)ではない: hasSome はタイムラインの複数メンバー選択フィルタ(selectedMemberIds との重複判定。lib/timeline-utils.ts の filterBySelectedMembers)のように「複数の値のいずれかに一致するか」を判定する場合に使う演算子。本セクションは単一メンバーID(id)が配列に含まれるかどうかの判定のみでよいため、has: id が対応する演算子となる
  • lib/timeline-utils.ts の withResolvedMembers/resolveEventMembers による動的補完は不要: この補完は live・festival(TENURE_FALLBACK_TYPES)のみを対象にした、「個別の出演者データを持たず、当時在籍していたメンバー全員が参加したとみなす」団体イベント向けのフォールバックである。source='manual'のイベントは sync-events.ts の ManualEventSchema の変換仕様の通り、常に members が明示的に設定される(manual-events.json の members が必須項目で省略不可)。そのため空配列フォールバックの対象になり得ず、素直な has フィルタで正しく取得できる
  • 開発ノート「UI/UX設計」#83(events.members が空配列のレコードを hasSome で絞り込むと除外される問題)はタイムラインの複数選択フィルタ向けの知見であり、該当するのは live・festival のみ。本セクションの単一メンバーの has 検索には該当しない

表示内容

全件が source='manual' であるため、§2の詳細画面が一律で存在する。type によるリンクの出し分けは不要。

項目 内容
日付 events.date(時刻がある場合は併記)。formatLiveDateFull/formatLiveTime/shouldShowTime(lib/live-helpers.ts)を流用する
種別バッジ typeがtopic→「トピック」・media→「メディア出演」。バッジ色は既存の EVENT_TYPE_COLORS(components/EventCard.tsx)を流用する。現状 EventCard.tsx にのみ定義され、かつ media キーが存在しないため、lib/event-colors.ts(新規)に切り出したうえで media: '#38bdf8'(sky-400。既存キー── live violet-400・festival orange-400・topic slate-400・join green-400・graduate red系・leader gold ──と色相が重ならない色)を追加し、EventCard.tsx 側もそちらを参照するよう変更する
タイトル events.title
リンク 全件 /events/{id} へリンク

配置・実装方針

  • コンポーネント: components/member-detail/EventsSection.tsx(新規)
  • 配置位置: RadioAppearancesSection の後(MemberDetailView.tsx のセクション列の末尾に追加)。ページ内クイックナビ(↓ イベント一覧)も同じ並びに追加する
  • 表示形式: 日付降順のフラットな1リストとする。RelatedLivesSection のようなグループ単位のアコーディオン(折りたたみ)は採用しない
    • 理由: アコーディオンは「1グループに多数の要素が属する」場合(ツアーの複数公演等)に折りたたみの効果があるが、手動登録イベントには複数公演のようなグループ単位が存在せず、1メンバーあたりの件数も少数にとどまるため、グループ化・折りたたみの恩恵がない
    • RelatedLivesSection.tsx は見出し(ヘッダー+一覧へのリンク)・リスト項目のスタイル(hr区切り・日付とタイトルの横並び等)の参考実装として踏襲するにとどめる
  • Props型: コンポーネントローカルに定義する(RadioAppearancesSection.tsxのRadioShowGroupと同じ方針。types/には置かない。他のファイルとimport共有しないUI専用の中間データ構造のため)
    interface MemberEventEntry {
      id: string;
      type: "topic" | "media";
      title: string;
      date: string;
      time: string | null;
    }
  • ゼロ件時: セクション自体を非表示(他の任意セクションと同じ方針)

4. クロスリンク図

画面設計書の「外部遷移図(タイムライン・年表・関係性マップ系列)」に以下のノード・エッジを追加する。

flowchart LR
  TIMELINE["タイムライン(年表ビュー含む)\n/timeline"]
  EVENT["イベント詳細\n/events/:id"]
  MEMBER["メンバー詳細\n/members/:id"]
  MEMBERMAP["メンバー関係性マップ\n/member-map"]
  MEMBERMAP_RADAR["レーダーチャート\n/member-map/radar"]

  MEMBERMAP -- レーダーチャートリンク --> MEMBERMAP_RADAR
  MEMBERMAP_RADAR -- マップに戻る --> MEMBERMAP
  MEMBERMAP -- タイムライン連携パネル「タイムラインで見る」リンク --> TIMELINE
  TIMELINE -- イベントカードクリック --> EVENT
  MEMBER -- イベント一覧 > イベントリンク --> EVENT
  EVENT -- メンバー名クリック --> MEMBER
  EVENT -- 前の画面に戻る(タイムラインから遷移時) --> TIMELINE
  EVENT -- 前の画面に戻る(メンバー詳細から遷移時) --> MEMBER

5. 後続 Issue との関係

Issue 本設計書への依存内容
MorningStatusApp#1583「イベント詳細画面・メンバー詳細イベント一覧 実装」 本設計書§2・§3に基づき実装する。依存: MorningStatusApp#1533(会場・楽曲属性実装、クローズ済)

改訂履歴

版 更新日 変更内容
1.2 2026-09-09 eventsテーブルへのリンクテキストをタイトル変更(「events テーブル定義書」→「イベントテーブル定義書」)に追随して更新(morning-status-blume#113レビューで検出)
1.1 2026-09-03 「戻る」の行き先を/timeline固定リンクから、ブラウザ履歴に基づく「前の画面に戻る」(components/BackLink.tsx流用)に修正。タイムライン画面のイベントカードから本画面への遷移リンクを追加する設計を追加し、遷移元が複数になったことに対応。クロスリンク図・画面構成の記述も追随更新(blume#101。MorningStatusApp#1583実装中に発覚した設計不備の修正)
1.0 2026-09-01 初版作成。イベント詳細画面(/events/[id])とメンバー詳細画面「イベント一覧」セクションの設計を追加。対象はtypeではなくsource='manual'を基準とし、radio_episodesに対応しない手動登録mediaイベント(音楽番組・バラエティ等への単発出演)も対象に含める(blume#97)

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