---
title: "イベント詳細画面 設計書"
---

最終更新: 2026-09-01
関連 Issue: morning-status-blume#97（本設計書作成）
親 Issue: MorningStatusApp#1530「イベント・トピックの表示」
依存: MorningStatusApp#1533（手動イベント登録の会場・楽曲属性実装。`events.venueId`・`event_songs`・`event_song_tracks`。クローズ済み）

---

## 1. 概要

`events`（[events テーブル定義書](/design/timeline/events-table)）のうち **`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のみ」のシンプルな構成を採用する

### データ取得

```ts
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にする） |

### 表示コンテンツ

| 要素 | 内容 |
|---|---|
| ナビゲーション | 「タイムラインに戻る」リンク（`/timeline` へ。年指定なし＝最新年で開く） |
| タイトル | `events.title` |
| 日付 | `events.date`（[ライブ・フェス機能 画面設計書](/design/live/live-screen-design)のライブ詳細と同じ`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.link`、任意）とメンバー関係性マップへのリンクのみを持ち、本画面（`/events/[id]`）への遷移リンクは持たない。親Issue（MorningStatusApp#1530）の要求は「メンバー詳細 → イベント一覧 → イベント詳細」の導線（§3）であり、タイムライン側からの導線追加は本Issueのスコープに含めない。将来的にタイムラインの該当カードから本画面へリンクする場合は、別Issueで `EventCard` の変更として扱う。

---

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

### 概要

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

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

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

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

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

```ts
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専用の中間データ構造のため）
  ```ts
  interface MemberEventEntry {
    id: string;
    type: "topic" | "media";
    title: string;
    date: string;
    time: string | null;
  }
  ```
- **ゼロ件時**: セクション自体を非表示（他の任意セクションと同じ方針）

---

## 4. クロスリンク図

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

```mermaid
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
  EVENT -- メンバー名クリック --> MEMBER
  EVENT -- タイムラインに戻る --> TIMELINE
  MEMBER -- イベント一覧 > イベントリンク --> EVENT
```

---

## 5. 後続 Issue との関係

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

---

## 改訂履歴

| 版 | 更新日 | 変更内容 |
| --- | --- | --- |
| 1.0 | 2026-09-01 | 初版作成。イベント詳細画面（`/events/[id]`）とメンバー詳細画面「イベント一覧」セクションの設計を追加。対象は`type`ではなく`source='manual'`を基準とし、`radio_episodes`に対応しない手動登録`media`イベント（音楽番組・バラエティ等への単発出演）も対象に含める（blume#97） |
