---
title: "TikTok投稿機能 データ設計書"
---

最終更新: 2026-08-12

## 1. 概要

公式 TikTok アカウント（モーニング娘。公式 `@morningmusume_uf`・ミニモちゃん `@minimochan_dapyon`）の投稿を RapidAPI（`tiktok-scraper7.p.rapidapi.com`）経由で取得し、Neon（PostgreSQL）に保存する。対象アカウントは `constants/tiktok-official-accounts.ts` の `OFFICIAL_TIKTOK_ACCOUNTS` で管理し、収集スクリプトはこの配列をループしてアカウントごとに処理する（#1458）。

上記に加えて、OGメンバー個人のTikTokアカウント（[メンバー データ設計書](/design/common/member-data-design) §3 の `Member.tiktokChannel` で管理）の投稿も収集する。YouTube OG収集（[YouTube投稿一覧機能 データ設計書](/design/sns/youtube-data-design) §4-5）と同じ `channelType`（`'official'` | `'og'`）+ `memberId` 方式で公式アカウントと区別する（#1478）。

---

## 2. Neon テーブル一覧

| テーブル名 | 概要 | 更新タイミング |
| --- | --- | --- |
| `tiktok_posts` | 収集済み投稿メタデータ（公式アカウント・OGメンバー個人アカウント共通） | 公式: `collect-tiktok-posts` ワークフロー実行時（差分追加、毎日）。OG: `collect-tiktok-og` ワークフロー実行時（差分追加、`OG_SYNC_WEEKDAYS` で指定した曜日のみ。#1478） |
| `tiktok_sync_status` | 公式アカウント収集の同期実行状態（`/sync-status`画面向け）＋TOP新着投稿セクションの新着ウィンドウ判定用のofficial/og独立ウィンドウ（常に1行。id=1 を upsert） | `collect-tiktok-posts`（official用フィールドを更新）・`collect-tiktok-og`（og用ウィンドウのみ更新）ワークフロー実行時。詳細は§4「新着表示の判定基準」参照（#1489） |

---

## 3. 型定義

型定義ファイル: `types/tiktok.ts`

### `TikTokPost`（投稿メタデータ）

```ts
type TikTokPost = {
  postId?: string;           // 投稿ID（video_id。TikTok URL の数値部分）
  postUrl: string;           // 投稿URL (https://www.tiktok.com/@{account.username}/video/{video_id})
  imageBlobUrl?: string;     // [未使用] Vercel Blob 上の画像 URL（将来用途のみ）
  thumbnailUrl?: string;     // サムネイル画像 URL（RapidAPI の cover フィールド。署名付き URL のため有効期限あり）
  caption: string;           // 投稿キャプション（RapidAPI の title フィールド。null は空文字に正規化）
  capturedAt: string;        // 取得日時（ISO 8601）
  createdAt?: string;        // 実際の投稿日時（ISO 8601。RapidAPI の create_time を Unix タイムスタンプから変換）
  mentionedMemberIds?: string[]; // 言及メンバーID一覧（収集時に `attributeTikTokPostToMembers` で算出。公式アカウントのみ。OGは空のまま。#1478）
  accountId?: string;        // 収集元の公式アカウントID（`OFFICIAL_TIKTOK_ACCOUNTS` の id。公式のみ。#1458）
  channelType?: 'official' | 'og'; // アカウント種別。既存行は 'official' がデフォルト（#1478）
  memberId?: string;         // OGメンバーのみ。公式アカウントの場合は undefined（#1478）
};
```

**Neon カラムとのマッピング:**

| Neon カラム | TikTokPost フィールド | 型 | 備考 |
| --- | --- | --- | --- |
| `post_id` | `postId` | VARCHAR(50) PK | RapidAPI の `video_id`。数値文字列 |
| `post_url` | `postUrl` | TEXT UNIQUE | TikTok 投稿 URL |
| `thumbnail_url` | `thumbnailUrl` | TEXT NULL | RapidAPI の `cover`。署名付き URL（有効期限あり） |
| `image_blob_url` | `imageBlobUrl` | TEXT NULL | 将来用途（未使用） |
| `caption` | `caption` | TEXT | 投稿キャプション・ハッシュタグ |
| `captured_at` | `capturedAt` | TIMESTAMPTZ | 収集日時 |
| `created_at` | `createdAt` | TIMESTAMPTZ NULL | 実際の投稿日時 |
| `mentioned_member_ids` | `mentionedMemberIds` | TEXT[] | 言及メンバーID。公式アカウントのみ算出。OGは常に空配列（#1478） |
| `account_id` | `accountId` | VARCHAR(50) NULL DEFAULT `morningmusume-official` | 収集元の公式アカウントID。`#1458` で追加。OGメンバー投稿は NULL（#1478 でカラムを nullable に変更。DEFAULT は既存行backfill用に残置、公式アカウント収集時は明示的に `account.id` を渡すため実質未使用） |
| `channel_type` | `channelType` | VARCHAR(20) DEFAULT `official` | `'official'` または `'og'`。既存行はマイグレーション時にデフォルト値で自動補完される（#1478） |
| `member_id` | `memberId` | VARCHAR(50) NULL | OGメンバーのみ。公式アカウントは NULL（#1478） |

**RapidAPI フィールドとのマッピング:**

| RapidAPI フィールド | TikTokPost フィールド | 備考 |
| --- | --- | --- |
| `video_id` | `postId` | 数値文字列。URL に使用する |
| `aweme_id` | —（未保存） | 英数混合の内部ID。postId には使わない |
| `title` | `caption` | 投稿キャプション・ハッシュタグを含む |
| `cover` | `thumbnailUrl` | 署名付き URL。有効期限（`x-expires`）あり |
| `create_time` | `createdAt` | Unix タイムスタンプ → `new Date(t * 1000).toISOString()` |

### `TikTokSyncStatus`（同期状態）

```ts
type TikTokSyncStatus = {
  lastSyncedAt: string;             // 最終同期日時（ISO 8601。/sync-status画面向け）
  syncStatus: 'valid' | 'expired'; // API 取得結果: 'valid'=投稿あり / 'expired'=投稿0件（/sync-status画面向け）
  capturedMembersCount: number;    // 取得対象アカウント数（`OFFICIAL_TIKTOK_ACCOUNTS.length`。#1458。/sync-status画面向け）
  totalTargetMembersCount: number; // 取得対象アカウント総数（`OFFICIAL_TIKTOK_ACCOUNTS.length`。#1458。/sync-status画面向け）
  retroCursor: string | null;      // [廃止] 遡及取得カーソル。初回遡及完了済みのため収集スクリプトでは使用しない（#1178）
  // TOP新着投稿セクションの新着ウィンドウ判定用（#1489）。詳細は§4「新着表示の判定基準」参照
  officialLastSyncedAt: string;         // official の直近の実行完了時刻（手動実行含む）
  officialLastAutoSyncedAt: string;     // official の直近の「定期実行」の完了時刻
  officialPreviousAutoSyncedAt: string; // official のその1つ前の「定期実行」の完了時刻
  ogLastSyncedAt: string;               // og の直近の実行完了時刻（手動実行含む）
  ogLastAutoSyncedAt: string;           // og の直近の「定期実行」の完了時刻
  ogPreviousAutoSyncedAt: string;       // og のその1つ前の「定期実行」の完了時刻
};
```

**Neon カラムとのマッピング（`tiktok_sync_status`）:**

| Neon カラム | 型 | 備考 |
| --- | --- | --- |
| `last_synced_at` | TIMESTAMPTZ | 最終同期日時（/sync-status画面向け） |
| `sync_status` | VARCHAR(20) | `valid` または `expired`（/sync-status画面向け） |
| `captured_members_count` | INT | 取得対象アカウント数（`OFFICIAL_TIKTOK_ACCOUNTS.length`。#1458。/sync-status画面向け） |
| `total_target_members_count` | INT | 取得対象アカウント総数（`OFFICIAL_TIKTOK_ACCOUNTS.length`。#1458。/sync-status画面向け） |
| `retro_cursor` | TEXT NULL | [廃止] 遡及取得カーソル。初回遡及完了済みのため収集スクリプトでは使用しない（#1178） |
| `official_last_synced_at` | TIMESTAMPTZ | official 用（#1489） |
| `official_last_auto_synced_at` | TIMESTAMPTZ | official 用（#1489） |
| `official_previous_auto_synced_at` | TIMESTAMPTZ | official 用（#1489） |
| `og_last_synced_at` | TIMESTAMPTZ | og 用（#1489） |
| `og_last_auto_synced_at` | TIMESTAMPTZ | og 用（#1489） |
| `og_previous_auto_synced_at` | TIMESTAMPTZ | og 用（#1489） |

---

## 4. 運用ルール

### 収集対象

**公式・関連アカウント（`channelType: 'official'`）**

- `constants/tiktok-official-accounts.ts` の `OFFICIAL_TIKTOK_ACCOUNTS` に登録された公式・関連アカウントを対象とする。各アカウントは `accountType`（`'normal'` | `'group'`）を持ち、`/tiktok` 画面での投稿の分類方法を左右する（§6参照）。2026-08-08 時点で以下の2件:
  - `morningmusume-official`（モーニング娘。公式、`@morningmusume_uf`。`accountType: 'group'` — メンバーが在籍する公式アカウント特有の扱いとして、投稿をメンバー・楽曲に分類する）
  - `minimochan-dapyon`（ミニモちゃん、`@minimochan_dapyon`。`accountType: 'normal'`（デフォルト）。#1458）
- 収集スクリプト（`collect-tiktok-posts.ts`）は `OFFICIAL_TIKTOK_ACCOUNTS` を順にループし、アカウントごとに新着取得を行う。収集処理（RapidAPIからの取得・保存・メンバー紐付け判定）自体は `accountType` によらず全アカウント共通。分類は表示側（§6）でのみ `accountType` により分岐する

**OGメンバー個人アカウント（`channelType: 'og'`、#1478）**

- `Member.tiktokChannel`（[メンバー データ設計書](/design/common/member-data-design) §3）が設定された OG メンバーを対象とする。メンバー編集画面から設定する（YouTube OG収集と同じ運用方式。[YouTube投稿一覧機能 データ設計書](/design/sns/youtube-data-design) §4参照）
- 収集スクリプト（`collect-tiktok-og.ts`）を新設し、`tiktokChannel` が設定された OG メンバーをループして新着取得を行う。公式アカウント収集（`collect-tiktok-posts.ts`）とは別スクリプト・別ワークフローで完全に独立させる（実行スケジュールが異なるため。§「収集スケジュール」参照）

### 新着取得

**公式アカウント**

収集スクリプトは新着取得のみを行う。公式 TikTok アカウントの初回遡及取得は完了済みのため、遡及取得は実施しない（#1178）。

| 動作 | API コスト |
| --- | --- |
| アカウントごとに最新ページから取得し、既存 `postId` に到達した時点で停止する。最大 `MAX_PAGES`（10）ページまで取得する | アカウントあたり通常 1 ページ |

`postId`（TikTok の video_id）はアカウント・チャンネル種別を問わずグローバルに一意なため、既存投稿の重複チェックは `tiktok_posts` テーブル全体に対して行う（アカウント別・OGメンバー別に絞り込まない）。

**OGメンバー個人アカウント（#1478）**

公式アカウントと異なり「既存 `postId` に到達するまでページを遡る」差分取得は行わない。保持件数の上限が5件（後述「保持件数」参照）であるため、既存の保存状況によらずメンバーごとに常に最新5件だけを取得し、DB側の保存内容をこの5件で置き換える（YouTube OG収集の `fetchVideosFromPlaylist(playlistId, 5)` と同じ考え方。§「保持件数」参照）。

| 動作 | API コスト |
| --- | --- |
| メンバーごとに `fetchUserPosts` を1ページ（先頭5件）のみ呼び出す。ページを遡ることはしない | メンバーあたり1ページ |

### 保持件数（#1478）

| チャンネル種別 | 保持ルール |
| --- | --- |
| 公式アカウント（`channelType: 'official'`） | 全件保持（差分追加のみ・削除しない） |
| OGメンバー個人アカウント（`channelType: 'og'`） | メンバーごとに、毎回取得した最新5件で保存内容を置き換える（取得した5件に含まれない既存投稿を削除したうえで、取得した5件を保存する。`skipDuplicates` 付き保存のため既存の5件と変わらない場合は実質何も変化しない）。YouTube OG収集（[YouTube投稿一覧機能 データ設計書](/design/sns/youtube-data-design) §5）と同じ方針 |

> **注意**: 初回設計時、「今回の新着取得分（差分）に含まれない投稿を削除する」という記述だったが、この方式だと初回同期時に最新5件を超える投稿が保存されたり、2回目以降の同期で新着が見つかるたびにそれ以前の保存済み投稿が全て削除されてしまう不具合があったため、「常に最新5件を取得してDB側を置き換える」方式に設計を修正した（#1478 のレビューで検出）。

### 収集スケジュール（#1478）

| チャンネル種別 | スケジュール | ワークフロー |
| --- | --- | --- |
| 公式アカウント | 毎日 | `collect-tiktok-posts.yml` |
| OGメンバー個人アカウント | `OG_SYNC_WEEKDAYS`（環境変数）で指定した曜日のみ。YouTube OG収集と同じ環境変数を共用する | `collect-tiktok-og.yml`（新設） |

### 重複防止

- **公式アカウント**: 収集スクリプト実行時に既存の `postId` を DB から取得し、一致する投稿はスキップする
- **OGメンバー個人アカウント**: 上記「保持件数」の置き換え処理（`skipDuplicates` 付き保存）によって重複が防止される。既存 `postId` に到達するまで遡る差分取得は行わないため、公式アカウントと異なり事前のスキップ判定はない

### 新着表示の判定基準（TOP新着投稿セクション、#1489）

TOP新着投稿セクションの新着判定は、[YouTube投稿一覧機能 データ設計書](/design/sns/youtube-data-design) §6.2 と同じ考え方で、`tiktok_sync_status`（§2・§3）の official/og 独立ウィンドウ `[xxxPreviousAutoSyncedAt, xxxLastSyncedAt]` を使う。`getRecentTikTokPosts()`（`lib/tiktok.ts`）が `channelType = 'official' AND capturedAt がofficial用ウィンドウ内` または `channelType = 'og' AND capturedAt がog用ウィンドウ内` のいずれかに合致するレコードを取得する（Prismaの`OR`条件）。**件数キャップは設けず、ウィンドウ内の該当レコードを全件返す。**

#### 書き込みロジック

`official`（`collect-tiktok-posts.ts`）・`og`（`collect-tiktok-og.ts`）は、`updateTikTokSyncStatus(channelType, officialSummary?)`（`lib/tiktok.ts`）にそれぞれ `'official'` / `'og'` を渡して呼び出し、**自分のチャンネル用ウィンドウのみ**を更新する（もう一方のチャンネルのウィンドウには触れない。YouTubeの`updateYoutubeSyncStatus`と同じ方式）。

- 実行トリガーの判定・定期実行/手動実行でのスライド方式は YouTube（[YouTube投稿一覧機能 データ設計書](/design/sns/youtube-data-design) §6.2）と同一
- `official` 呼び出し時のみ、`officialSummary`（`syncStatus`・`capturedMembersCount`・`totalTargetMembersCount`）を渡して `/sync-status` 画面向けの既存フィールド（`lastSyncedAt`・`syncStatus`・`capturedMembersCount`・`totalTargetMembersCount`）も併せて更新する。`og` 呼び出し時はこれらの値には触れず既存値を据え置く
- 初回実行時（テーブルが空）は、呼び出したチャンネル・呼び出していないチャンネルの双方が今回の実行時刻にフォールバックする

#### 旧方式からの変更経緯（#1489）

初版（#1408）は「TikTok全チャンネル横断で最新の `capturedAt` を1件求め、そのJST日付と同日のレコードを新着候補としたうえで `.slice(0, 10)` で先頭10件に切り詰める」方式だった。この方式には以下の構造的な問題があった。

- `allTikTokPosts`（`getTikTokPosts()`）は公式アカウント・OGメンバー個人アカウントを`channelType`で絞り込まず全件を対象にするため、複数アカウント・複数OGメンバーの投稿が全て同じ「同日」候補に混ざる
- 「同日」判定は日付境界のみで、Ameba（#1387）・YouTube（#1450）のような実行区間ベースのウィンドウではなかった
- `.slice(0, 10)` は候補の全件を単純に先頭10件へ切り詰めるだけで、実際の新着件数が10件を超えても警告なく黙って切り捨てる

このため、1回の収集で同日にまとまった件数の新着（複数アカウント・複数OGメンバーの初回同期、または通常運用でもたまたま複数アカウントに新着が重なった場合）が発生すると、実際には新着であるにもかかわらず10件を超えた分がホームページの新着投稿セクションから黙って消える不具合があった（ミニモちゃんTikTok公式アカウント収集の実運用開始時に200件超が初回同期され、10件しか表示されなかった事象で発覚）。

YouTube（#1450）と同じ official/og 独立ウィンドウ方式に変更し、件数キャップ自体を撤廃することでこの問題を解消した。`tiktok_sync_status` は元々 `/sync-status` 画面向けの公式収集ステータス（`lastSyncedAt`・`syncStatus`等）専用テーブルだったため、既存フィールドは変更せずに official/og 独立ウィンドウ用の6カラムを追加する形をとった（YouTubeの`youtube_sync_status`は本機能専用の新設テーブルだった点が異なる）。

### メンバーの紐付け

**公式アカウント（言及メンバーID）**

- 収集時（`collect-tiktok-posts.ts`）に `attributeTikTokPostToMembers`（`lib/tiktok-attribution.ts`）が `caption` を判定し、言及メンバーIDとしてDBに保存する
- 表示側（TikTok投稿一覧画面・メンバー詳細画面・トップページ新着投稿セクション）は、保存済みの `mentionedMemberIds` をそのまま表示に用いる
- **判定対象はキャプション内のハッシュタグ（`#` で始まるトークン）のみ**。本文中に名前が書かれていてもハッシュタグに含まれていなければマッチしない（#1412）
  - 理由: メンバー分類は投稿の閲覧を絞り込むための補助的な機能であり、ハッシュタグでない言及の方が稀なため、多少の見落としより無関係な投稿を誤って特定メンバーの投稿として表示してしまう方を避ける
- ハッシュタグ内でのマッチ規則:
  - メンバーのフルネーム（正規化後）が部分一致すればマッチ
  - フルネームが一致しない場合、苗字または名前（2文字以上のもの）が部分一致すればマッチ
  - 異体字（﨑/崎・髙/高・栁/柳等）は正規化してから比較する
- Instagram はメンバー個別の公式アカウントを直接取得する方式のため、同様のキャプション文字列マッチングは行っていない

**OGメンバー個人アカウント（#1478）**

- `attributeTikTokPostToMembers` によるハッシュタグ判定は行わない。`channelType: 'og'` + `memberId` で当該メンバー本人の投稿であることが自明なため（YouTube OG収集と同じ考え方）
- `mentionedMemberIds` は常に空配列のまま保存する

### サムネイル URL の有効期限・表示判定

- RapidAPI の `cover` フィールドは署名付き URL であり、`x-expires` パラメータで有効期限が設定されている
- DB には署名付き URL をそのまま保存する（有効期限後は表示できなくなる点に注意）
- スライドショー（photomode）投稿のサムネイルは `.heic` 形式で返される場合がある
  - 収集時に `isTikTokSlideshowThumbnail` でサムネイル URL を検査し、`isSlideshow: true` として DB に保存する
  - デスクトップブラウザ（Chrome・Firefox）は HEIC を表示できないため、`onError` ハンドラでリンクのみ表示へフォールバックする
  - モバイル・デスクトップいずれの表示形態でも、スライドショー投稿には「Slide」ラベルを表示する（画像表示時はサムネイル左上にオーバーレイ、リンクカード表示時はキャプション上部に付与）

---

## 5. 環境変数

| 変数名 | 用途 |
| --- | --- |
| `RAPIDAPI_KEY` | RapidAPI アクセスキー。Repository secret に保存 |
| `DATABASE_URL` | Neon への接続 URL。Repository secret に保存 |
| `MEMBERS_BLOB_URL` | Vercel Blob のメンバーデータ URL（OG収集で `tiktokChannel` を参照）。Repository secret に保存（#1478） |
| `OG_SYNC_WEEKDAYS` | OG収集を実行する曜日番号（カンマ区切り、デフォルト: `1`）。YouTube OG収集と共用。Repository variable に保存（非機密な設定値のため）（#1478） |

---

## 6. 画面設計（UI）

### TikTok投稿一覧画面（`/tiktok`）

**概要**: `OFFICIAL_TIKTOK_ACCOUNTS` の各アカウントごとに独立したブロックを設けて投稿一覧を表示する画面。投稿の分類方法はアカウントの `accountType` によって分岐する（#1458）。

| accountType | 対象アカウント | 表示方法 |
| --- | --- | --- |
| `group` | モーニング娘。公式 | 投稿を「メンバーごとの投稿」「楽曲プロモーション投稿」「グループとしての投稿」の3グループに分類して表示する。メンバーが在籍する公式アカウント特有の扱い |
| `normal`（デフォルト） | ミニモちゃん等 | 分類を行わず、投稿をそのままグリッド表示する |

> 「メンバーごとの投稿」「楽曲プロモーション投稿」という分類軸は、メンバーが在籍するモーニング娘。公式アカウント特有の概念であり、キャプションが偶然メンバー名や曲名に一致した場合だけ機械的に分類される汎用ロジック（`groupTikTokPosts`）に依存している。ミニモちゃんのようなマスコットキャラクターのアカウントに同じ分類軸を適用するのはアカウントの性質上不自然なため、`accountType: 'normal'`（デフォルト）のアカウントは分類せずフラット表示する（#1458 のレビューで指摘・是正）。

| 要素 | 内容 |
| --- | --- |
| ナビゲーション | 「← ホームへ戻る」リンク（`/` へ） |
| タイトル | 「TikTok」（固定表記） |
| 説明文 | 「公式・関連アカウントの投稿一覧」 |
| 著作権表記 | `DisclaimerNote`（「（画像は参考表示：権利は投稿者に帰属しています）」） |
| アカウント見出し | `OFFICIAL_TIKTOK_ACCOUNTS` の順に、アカウント名 + `@username` を表示。当該アカウントの投稿が1件もない場合はブロックごと非表示 |
| データなし表示 | 全アカウント合計で投稿が0件の場合「投稿データが見つかりませんでした。」の glass カードを表示 |
| OGメンバーへのリンク（#1478） | `tiktokChannel` が設定された OG メンバーのリンクカードをグリッド表示（メンバー名＋ `@username`）。`/tiktok/og/[memberId]` へ遷移。YouTube投稿一覧画面（`/youtube`）の OGメンバーチャンネルセクションと同じ構成。対象 OG メンバーが0件の場合はセクションごと非表示 |

**表示順（#1491）**: アカウント見出し（公式アカウント）セクションを先に表示し、OGメンバーへのリンクセクションはその後に表示する。SNS投稿一覧の共通ルール「公式アカウント→メンバー（現役→OG）」に合わせるため。

**`accountType: 'group'` の表示（モーニング娘。公式）**:

| 要素 | 内容 |
| --- | --- |
| メンバーごとの投稿 | `accountId` で絞り込んだ投稿をメンバー別にアコーディオン（`<details>`）表示。サムネイル有効な投稿を含むメンバーを優先ソート。先頭のみデフォルト展開 |
| 楽曲プロモーション投稿 | 曲別にアコーディオン表示（曲名は `/songs/{trackId}` へリンク）。サムネイル有効な投稿を含む曲を優先ソート |
| グループとしての投稿 | 個別メンバー・楽曲に紐付かない投稿をグリッド表示 |

アコーディオン見出し（`<summary>`）の表示内容:

| 項目 | 内容 |
| --- | --- |
| 見出し名 | メンバー名 or 曲タイトル |
| サムネイルありアイコン | 📹（グループ内にサムネイル有効な投稿が1件以上ある場合のみ） |
| 件数 | `（{posts.length}件）` |

**`accountType: 'normal'` の表示（ミニモちゃん等）**:

`accountId` で絞り込んだ投稿を分類せず、そのまま投稿カードのグリッドで表示する（アコーディオン・セクション見出しなし）。

**投稿カード（`PostCard`、ページ内ローカル実装）の表示内容**:

| 状態 | 表示内容 |
| --- | --- |
| サムネイル有効（`isTikTokThumbnailValid`） | 正方形サムネイル（`imageBlobUrl` 優先・なければ `thumbnailUrl`）＋取得日時＋キャプション（全文、`pre-wrap`）＋登場メンバーへのリンクバッジ（`mentionedMemberIds` から解決） |
| サムネイル無効・期限切れ | キャプション（またはキャプション未設定時は「TikTok投稿」）のみのglassリンクカード |

いずれもクリックで `postUrl`（TikTok投稿ページ）を新しいタブで開く。

> メンバー詳細画面のTikTokセクション（`components/member-detail/TikTokSection.tsx`・`TikTokPostCard.tsx`）は本画面とは別実装で、スライドショー投稿向けの「Slide」ラベル表示を持つ。本画面（`/tiktok`）のローカル `PostCard` にはスライドショー判定は実装されていない。

### TikTok OGアカウント画面（`/tiktok/og/[memberId]`、#1478）

**概要**: OGメンバー個人アカウントの保存済み最新5件をグリッド表示する（保持件数が最新5件のため、ページ切り替えなし）。YouTube OGチャンネル画面（[YouTube投稿一覧機能 画面設計書](/design/sns/youtube-screen-design) §5）と同じ構成。

| 要素 | 内容 |
| --- | --- |
| ナビゲーション | 「← TikTok投稿一覧へ戻る」リンク（`/tiktok` へ） |
| タイトル | `{メンバー名} TikTok` |
| 説明文 | `@username` を表示 |
| 著作権表記 | `DisclaimerNote` |
| 投稿グリッド | `channelType: 'og'` かつ `memberId` が一致する投稿を投稿日時降順で最大5件、`PostCard`（§6の`/tiktok`と同一実装）で表示 |
| メンバー詳細へのリンク | 「メンバー詳細へ」リンク（`/members/{memberId}` へ） |
| `memberId` が存在しない・`tiktokChannel` 未設定 | `notFound()`（404） |

### メンバー詳細画面への表示（#1478）

メンバー詳細画面のTikTokセクション（`components/member-detail/TikTokSection.tsx`）には、以下の2種類の投稿をあわせて表示する。

| 投稿の種類 | 判定条件 |
| --- | --- |
| 公式アカウントでの言及投稿（既存） | `mentionedMemberIds` に対象メンバーの `id` を含む |
| OGメンバー本人の投稿（#1478） | `channelType === 'og'` かつ `memberId` が対象メンバーの `id` と一致する |

YouTubeはメンバー詳細画面にOG動画を表示しない実装だが（判定ロジックが独立しており技術的な混在が起きないため）、TikTokはメンバー詳細画面に統合表示する。`/tiktok/og/[memberId]` 専用ページ（上記）は、`/tiktok` トップページからの独立した閲覧導線として別途維持する。

---

## 改訂履歴

| 版 | 更新日 | 変更内容 |
| --- | --- | --- |
| 2.2 | 2026-08-12 | SNS投稿一覧のルール統一（#1491）: `/tiktok` 画面のタイトルを「モーニング娘。公式 TikTok」から媒体名のみの「TikTok」に変更。表示順をアカウント見出し（公式アカウント）セクション→OGメンバーへのリンクセクションの順に統一する旨を明記（実装ではOGメンバーが先に描画されていたため是正） |
| 2.1 | 2026-08-09 | TOP新着投稿セクションの新着判定を「同日capturedAt + slice(0, 10)」の件数キャップ方式から、YouTube（#1450）と同じofficial/og独立ウィンドウ方式に置き換え（§4「新着表示の判定基準」を新設）。`TikTokSyncStatus`/`tiktok_sync_status`にofficial/og独立ウィンドウ用の6カラム（`officialLastSyncedAt`等）を追加。既存の`/sync-status`画面向けフィールドは変更しない。件数キャップにより同日にまとまった件数の新着（複数アカウント・複数OGメンバーの同時収集等）が黙って表示から漏れる不具合の修正（#1489） |
| 2.0 | 2026-08-09 | OGメンバー個人アカウントのTikTok収集を追加（#1478）。YouTube OG収集（`channelType`+`memberId`方式）と同じ方針を採用: `TikTokPost`/`tiktok_posts`に`channelType`（`'official'`\|`'og'`）・`memberId`カラムを追加、`accountId`をnullable化。アカウント情報はメンバー編集画面から設定する`Member.tiktokChannel`（詳細は[メンバー データ設計書](/design/common/member-data-design)）で管理。OGは最新5件のみ保持、`OG_SYNC_WEEKDAYS`で指定した曜日のみ専用ワークフロー（`collect-tiktok-og.ts`）で収集し、`tiktok_sync_status`は更新しない。`/tiktok/og/[memberId]`画面を新設し、メンバー詳細画面のTikTokセクションにはOG本人の投稿も統合表示する（YouTubeと異なり分離しない） |
| 1.9 | 2026-08-08 | 複数公式アカウント対応（#1458）: ミニモちゃん（`@minimochan_dapyon`）を収集対象に追加。`TikTokPost`/`tiktok_posts` に `accountId` フィールド・カラムを追加。`OFFICIAL_TIKTOK_ACCOUNTS` の各アカウントに `accountType`（`'normal'` | `'group'`）を追加し、`/tiktok` 画面はアカウント別ブロック表示のうえ `accountType` により分類方法を分岐（`group`=メンバー・楽曲別に分類、`normal`=フラット表示）。メンバー在籍アカウント特有の分類軸をマスコットアカウントに一律適用しないための設計修正（レビュー指摘により是正） |
| 1.8 | 2026-07-31 | メンバーの紐付け（§4）に、表示側は保存済みの `mentionedMemberIds` をそのまま表示に用いる旨を明記（morning-status-blume#19） |
| 1.7 | 2026-07-25 | メンバーの紐付け（§4）を追加。判定対象をハッシュタグのみに限定する仕様変更を反映（#1412）。言及メンバーIDの「将来実装」表記を是正（実装済み） |
| 1.6 | 2026-07-18 | 画面設計セクション（§6）を追加。`/tiktok` 画面の表示内容を反映（#1366） |
| 1.5 | 2026-06-23 | 遡及取得（Phase 2）を廃止（#1178）。初回遡及完了済みのため新着取得のみに変更。`retro_cursor` を廃止フィールドとしてマーク |
| 1.4 | 2026-06-20 | スライドショー投稿の種別管理を追加（#1183）: `isSlideshow` フィールドを DB・型定義・収集スクリプトに追加。表示側でモバイル（画像オーバーレイ）・デスクトップ（リンクカード）いずれも「Slide」ラベルを表示する仕様を明文化 |
| 1.3 | 2026-05-28 | 2フェーズ収集（新着 + 遡及）設計を追加（#933）。`TikTokSyncStatus` に `retroCursor` フィールドを追加。`tiktok_posts` 保存上限（200件）撤廃記述を削除（#924 で対応済み） |
| 1.2 | 2026-05-15 | ストレージを Vercel Blob → Neon に移行（#888）: テーブル定義・カラムマッピング・syncStatus 改名 |
| 1.1 | 2026-05-15 | RapidAPI レスポンスフィールド修正（#890）: `aweme_list`→`videos`・`desc`→`title`・`video.cover.url_list`→`cover`・`aweme_id`→`video_id`（postId）に対応 |
| 1.0 | 2026-05-15 | 初版作成（#887）: Playwright から RapidAPI への切り替えに伴い新規作成 |
