diff --git a/%E5%AE%9F%E8%A3%85%E8%AA%AC%E6%98%8E%E6%9B%B8.md b/%E5%AE%9F%E8%A3%85%E8%AA%AC%E6%98%8E%E6%9B%B8.md index 19c435a..c62cb9f 100644 --- a/%E5%AE%9F%E8%A3%85%E8%AA%AC%E6%98%8E%E6%9B%B8.md +++ b/%E5%AE%9F%E8%A3%85%E8%AA%AC%E6%98%8E%E6%9B%B8.md @@ -1,1674 +1,1522 @@ --- -title: 'BTRC Hub 仕様書' -subtitle: '現行仕様と計画仕様の統合版(2026-04-20)' -date: '2026-04-20' +title: 'BTRC Hub / タグ広場 現行実装仕様書' +subtitle: '現行ソース btrc-hub-main.zip に基づく実装事実ベース仕様(2026-05-10)' +date: '2026-05-10' lang: ja-JP toc: true -toc-depth: 2 +toc-depth: 3 numbersections: true -documentclass: report -geometry: - - margin=22mm -fontsize: 10pt -mainfont: 'Noto Sans CJK JP' -monofont: 'Noto Sans Mono CJK JP' -header-includes: - - | - \usepackage{longtable} - \usepackage{booktabs} - \usepackage{array} - \usepackage{xcolor} - \usepackage{hyperref} - \definecolor{linkcolor}{HTML}{0B5FFF} - \hypersetup{colorlinks=true,linkcolor=linkcolor,urlcolor=linkcolor} --- # 本書の位置づけ -本書は、2026-04-19 時点で置かれている資材と、2026-04-20 の追加開発者ヒアリングをもとに、BTRC Hub の仕様を最新化したものである。 +本書は、2026-05-10 時点で添付された `btrc-hub-main.zip` の実装を確認し、タグ広場(BTRC Hub)の現行仕様を再構成した文書である。 -- `btrc-hub-main.zip`: 現行ソースコード -- `btrc-hub.wiki.zip`: 開発 Wiki -- `btrc-hub.issue.sql`: Gitea issue エクスポート -- `BTRC_Hub_仕様書_2026-03-23.md`: 既存仕様書 +既存の `BTRC_Hub_仕様書_2026-03-23.md` は有用な土台だが、現行実装とはすでに差分がある。したがって本書では、過去仕様書の記述よりも現行ソース・`db/schema.rb`・`routes.rb`・フロントエンドルーティング・主要コントローラ/モデルの実装を優先する。 -本書の目的は、実装済みの事実と将来計画を分離して、開発・公開判断・今後の設計変更に耐える基準文書にすることである。 - -## 情報の優先順位 - -| 優先順位 | 情報源 | 扱い | -| ---: | --- | --- | -| 1 | 現行ソースコード、`db/schema.rb`、ルーティング | 現行仕様の根拠 | -| 2 | フロントエンドの画面・型・API 呼出 | 画面仕様と実利用導線の根拠 | -| 3 | Wiki と issue | 設計意図、未実装計画、運用課題の根拠 | -| 4 | 既存仕様書 | 体系化済みの文脈として再利用 | - -本書では、仕様を次の 3 種に分ける。 +本書の分類は次のとおり。 | 表記 | 意味 | | --- | --- | -| 現行仕様 | 現在のコード、スキーマ、画面から確認できるもの | -| 計画仕様(確定) | 実装は未完だが方針として採用済みのもの | -| 計画仕様(未確定) | 方向性はあるが、実装方針・責務・公開範囲が未整理のもの | +| 現行仕様 | 現在のソース・スキーマ・画面で確認できるもの | +| 実装あり・UI薄い | バックエンドやモデルは存在するが、画面や導線が不足しているもの | +| 計画・未確定 | ソースだけでは確定できず、開発者ヒアリングが必要なもの | +| 注意 | 実装上の落とし穴、仕様として明文化すべき制約 | -## 2026-03-23 版からの主要更新 +本書は「願望」ではなく、コードにあるものをまず書く。曖昧な点は `開発者ヒアリング` として末尾に隔離する。 -今回の最新化で、次の項目を現行仕様へ昇格した。 +## 用語: 公開と公表 -| 項目 | 2026-04-19 時点の扱い | +本書では、以後 **公開** と **公表** を分ける。 + +| 用語 | 意味 | | --- | --- | -| 投稿版管理 `post_versions` | 実装済み。`/posts/versions` と耕作履歴画面で使用 | -| 投稿履歴からの復元 | フロントで実装済み。ただし復元は専用 API ではなく `PUT /posts/:id` を使う | -| 素材集 `materials` | API と画面が実装済み。ただしトップナビでは非表示 | -| 素材の版テーブル `material_versions` | テーブルのみ存在。モデル・記録処理・API は未接続 | -| Wiki 画像 `wiki_assets` | テーブルのみ存在。現行ルート・モデル・画面は未接続 | -| 上位タグ D&D 操作 | 投稿詳細サイドバーで実装済み。API は admin のみ許可 | -| タグ階層取得 `/tags/with-depth` | 実装済み。素材サイドバーで使用 | -| 利用規約 `/tos` | MDX ページとルートは存在。ただしトップナビでは非表示 | -| R2 ストレージ | production で Active Storage service `:r2` を使用 | -| whenever スケジュール | `nico:sync` と `post_similarity:calc` が `config/schedule.rb` に定義済み | -| `tag_similarities` | テーブル・モデル・計算タスクは存在。ただし現行 UI での明確な表示導線は薄い | -| 素材作成権限 | 公開時も guest に素材作成を許可する方針 | -| Wiki 画像記法 | 専用記法を採用する方針。具体案は `` 系 | +| 公開 | URL として到達可能であり、検索エンジン等にも拾われ得る状態。現時点のタグ広場はすでにこの状態である。 | +| 公表 | ぼざクリ界隈等へ明示的に告知し、利用導線を出して、人を呼び込む状態。 | -逆に、次のものは現行仕様として書くと嘘になる。 +したがって、旧来の「一般公開前」「公開前」という表現は、本書では原則として **公表前** と読み替える。すでにサービスは外部から到達可能であり、問題は「存在をどこまで告知し、利用を促すか」である。 + + -- Wiki 画像アップロードの実用 UI/API -- 素材履歴の表示・復元 -- 汎用設定 `settings` の取得・保存・反映 API -- BAN の認可強制 -- 投稿編集の排他制御 -- 別名タグ管理 UI -- Wiki 本文検索 -- Theatre の会場一覧・作成・編集 -- 管理画面 -- 通知機能 # システム概要 ## 目的 -BTRC Hub は、ぼざろクリーチャーシリーズ関連コンテンツへのリンクを集約し、タグ・Wiki・素材・外部同期によって横断的に検索・整理するためのシステムである。 +BTRC Hub は、ぼざろクリーチャー関連コンテンツへのリンクを収集し、タグ・Wiki・素材・外部同期・上映会機能を通じて、作品群や関連知識を整理・再発見しやすくするための共同編集型基盤である。 -重要なのは、保持する対象がコンテンツ本体ではなく **リンクと付帯知識** である点である。開発 Wiki でも、danbooru 的な整理性は参考にしつつ、コンテンツ本体の転載ではなくリンク集約に留める方針が示されている。 +中核は次の 5 系統である。 -## 中核価値 - -現行 BTRC Hub の中核価値は次の 5 つである。 - -1. 複数プラットフォーム上の関連コンテンツをリンクとして集約する。 -2. タグ・別名・上位タグ・外部タグ連携で再発見性を高める。 -3. Wiki によりタグ単位の説明と知識を蓄積する。 -4. ニコニコ外部 DB から投稿・タグ情報を日次同期する。 -5. 素材集・上映会など、整理基盤の周辺機能を育てる。 +1. **投稿**: 外部 URL を中心としたリンクデータ。 +2. **タグ**: カテゴリ、別名、親子関係、外部タグ連携、素材連携を持つ分類単位。 +3. **Wiki**: タグ名と結びつく説明ページ。行単位ストアと改訂履歴を持つ。 +4. **素材**: キャラクター・素材タグに紐づくファイルまたは参考 URL。 +5. **上映会**: 投稿を共同視聴し、コメント・在席・ホスト制御を行う実験的機能。 ## 非目的 -現行の実装思想上、次は中核目的ではない。 +現行実装は次を主目的にしていない。 -- 汎用 SNS 化 -- コメント欄中心の交流サービス化 -- コンテンツ本体の保存・転載 -- 動画配信基盤そのものの実装 -- ニジカ Wiki や虹鹿園と同一目的の重複サービス化 +- 汎用 SNS。 +- コメント掲示板。 +- 外部コンテンツ本体の転載保存サービス。 +- 高度な動画配信基盤。 +- 通常のメール/パスワード式アカウント管理。 -ただし、Theatre や素材集のように、整理基盤から派生する共同利用機能は実装され始めている。 +ただし、上映会や素材投稿などにより、単純なリンク集より重い知識基盤へ寄っている。 ## 技術構成 | 層 | 現行構成 | | --- | --- | -| バックエンド | Rails 8.0.2, MySQL 8, Active Storage | -| フロントエンド | React 19.1, Vite 6.3, TypeScript 5.8 | -| 通信・状態管理 | Axios, TanStack Query 5 | -| UI | Tailwind CSS 3, Framer Motion 12, Radix UI 系コンポーネント | -| Markdown | react-markdown, remark-gfm, react-markdown-editor-lite, 独自 Wiki 自動リンク | -| D&D | @dnd-kit/core | -| 画像処理 | MiniMagick | -| 外部取得 | Nokogiri, open-uri, Node screenshot script | -| 論理削除 | discard | -| PDF/差分等 | diff-lcs | -| ストレージ | development/test は Disk、production は Cloudflare R2 相当の S3 互換ストレージ | -| バッチ | Rake task、whenever schedule | +| バックエンド | Ruby 3.2.2 / Rails 8.0.2 API | +| DB | MySQL 8 / utf8mb4_0900_ai_ci | +| ファイル | Active Storage。Cloudflare R2/S3 利用を想定する構成あり | +| フロントエンド | React 19.1 / Vite 6.3 / TypeScript 5.8 | +| 通信 | Axios + TanStack Query | +| UI | Tailwind CSS / Framer Motion / shadcn 風 UI コンポーネント | +| Markdown | react-markdown / react-markdown-editor-lite / remark-wiki-autolink | +| バッチ | Rake task。Nico 同期、YouTube 同期、類似度計算など | -# 利用者と認証 +# 認証・ユーザ・BAN ## ユーザモデル -`users.role` は文字列 enum であり、現行ロールは次の 3 種である。 +`users` は次の主要属性を持つ。 -| ロール | 意味 | +| 属性 | 意味 | | --- | --- | -| guest | 初期発行ユーザ。閲覧中心 | -| member | 投稿・Wiki・タグ更新など通常の編集可 | -| admin | member 権限に加え、上位タグ設定など管理寄り操作可 | +| name | 表示名 | +| inheritance_code | 引継ぎコード。認証トークンとして使う | +| role | `guest` / `member` / `admin` | +| banned_at | BAN 時刻。NULL なら有効ユーザ | -`User#gte_member?` は `member` または `admin` を真とする。 +ロールは文字列 enum である。 + +| ロール | 権限の概略 | +| --- | --- | +| guest | 閲覧中心。編集系は不可 | +| member | 投稿・Wiki・タグ・素材など通常編集が可能 | +| admin | member 権限に加え、タグ親子関係など管理系操作が可能 | + +`User#gte_member?` は `member? || admin?` を返す。 ## 認証方式 -認証は ID / パスワードではなく、**引継ぎコード** による軽量認証である。 +認証は通常の ID / パスワードではなく、`inheritance_code` による軽量認証である。 -- ユーザ作成時に UUID 形式の `inheritance_code` を発行する。 -- フロントは `localStorage.user_code` に保持する。 -- API 呼び出し時、`X-Transfer-Code` ヘッダへ付与する。 -- `ApplicationController#authenticate_user` はヘッダ値から `User.find_by(inheritance_code:)` する。 - -この仕組みは簡単で UX は軽いが、通常のパスワード認証より防御面は薄い。公開段階では、権限範囲を絞る設計が必須である。 +- フロントは `localStorage.user_code` にコードを保持する。 +- API 呼び出し時、`X-Transfer-Code` ヘッダにコードを付与する。 +- サーバは `users.inheritance_code` と照合し、`current_user` を決定する。 ## 初回利用フロー -現行フロントの起動時処理は次の通り。 +フロント起動時の流れは次である。 -1. `localStorage.user_code` を読む。 -2. 存在する場合は `POST /users/verify` で検証する。 -3. 無効なら `POST /users` で guest ユーザを新規発行する。 -4. 存在しない場合も `POST /users` で guest ユーザを発行する。 -5. API レスポンスは camelCase に変換してフロント型へ流す。 +1. `localStorage.user_code` を確認する。 +2. ある場合は `POST /users/verify` へ送る。 +3. 有効なら返却ユーザを採用する。 +4. 無効またはコード無しなら `POST /users` で guest ユーザを新規作成する。 +5. 新規作成時、返却された引継ぎコードを localStorage に保存する。 + +## BAN 強制 + +過去仕様書との差分として重要。 + +現行 `ApplicationController` では、すべての API に対して次が `before_action` で走る。 + +1. `reject_banned_ip_address!` +2. `authenticate_user` +3. `reject_banned_user!` + +したがって現行実装では、**BAN は保存されているだけではなく、API レベルで強制される**。 + +| 対象 | 判定 | +| --- | --- | +| IP BAN | `ip_addresses.banned_at` が存在すれば 403 | +| User BAN | `users.banned_at` が存在すれば 403 | + +IP は `request.remote_ip` を `IPAddr#hton` で 16 byte binary 化し、`ip_addresses.ip_address` に保存する。 ## IP 紐づけ -`POST /users/verify` 実行時、サーバは `request.remote_ip` を 16 バイトのバイナリへ変換し、`ip_addresses` と `user_ips` に記録する。 +`POST /users` と `POST /users/verify` のタイミングで、ユーザと IP が紐づけられる。 -したがって、現行システムは **引継ぎコードと IP の対応を保持する土台** を持つ。ただし、`users.banned` と `ip_addresses.banned` はまだ認可処理へ強制接続されていない。 +- `ip_addresses`: IP アドレス本体。 +- `user_ips`: user と ip_address の複合主キー関係。 -## ユーザ設定画面 +注意点として、`request.remote_ip` が不正・空になるケースへの明示的な救済は薄い。ここはプロキシ構成・Cloudflare 経由・bot アクセスで事故り得る。 -現行 UI の `/users/settings` では次を扱う。 +# 主要ドメインモデル -- 表示名更新 -- 引継ぎコード表示 -- 引継ぎコード更新 -- 他ブラウザからの引継ぎ +## 投稿 posts -`settings` テーブルは存在するが、汎用設定の API・反映処理は未接続である。 +投稿は外部 URL を中心とするリンクデータである。 -# ドメインモデル - -## 投稿 `posts` - -投稿は URL を中心にしたリンクオブジェクトである。 - -| 属性 | 概要 | +| 属性 | 意味 | | --- | --- | -| `url` | 参照先 URL。一意。HTTP/HTTPS のみ | -| `title` | 表示用タイトル。NULL 可 | -| `thumbnail` | Active Storage 添付 | -| `thumbnail_base` | 外部サムネイル元 URL。NULL 可 | -| `parent_id` | 親投稿参照。現行では 1 親 | -| `uploaded_user_id` | 投稿者。同期投稿では NULL | -| `original_created_from` | 元コンテンツ作成日時の下限 | -| `original_created_before` | 元コンテンツ作成日時の上限(より前) | -| `created_at` / `updated_at` | 投稿レコードの作成・更新日時 | +| title | 表示タイトル | +| url | 投稿 URL。一意 | +| thumbnail_base | 外部サムネイル URL | +| uploaded_user_id | 手動投稿者。同期投稿は NULL になり得る | +| original_created_from | 元コンテンツ作成日時の下限 | +| original_created_before | 元コンテンツ作成日時の上限 | +| thumbnail | Active Storage 添付 | -### URL 正規化 +### URL 仕様 -保存前に次を行う。 +- URL は必須。 +- URL は一意。 +- HTTP / HTTPS のみ許可。 +- 保存前に前後空白を除去する。 +- host は小文字化される。 +- path 末尾の `/` は除去される。 -- 前後空白の除去 -- HTTP/HTTPS URL であることの検証 -- host の小文字化 -- path 末尾 `/` の除去 +### 元コンテンツ日時 -### オリジナル作成日時 +`original_created_from` と `original_created_before` の両方がある場合、`from < before` が必須である。 -`original_created_from` と `original_created_before` が両方ある場合、`from < before` が必須である。 +YouTube/Nico 同期では、動画公開時刻を分単位の範囲として保持する実装がある。 -この仕様は「日時そのもの」ではなく、「その範囲内にリンク先コンテンツが公開された」ことを表す。ニコニコ同期では分単位で `from` と `before = from + 1 minute` を生成する。 +### サムネイル -## 投稿版 `post_versions` +手動投稿時に `thumbnail` が渡されると Active Storage に添付され、`Post#resized_thumbnail!` により 180x180 の JPEG へリサイズされる。 -`post_versions` は投稿のスナップショット履歴である。 +## 投稿親子関係 post_implications -| 属性 | 概要 | +現行実装では、投稿の親子関係は `posts.parent_id` ではない。 + +`post_implications` により、投稿と親投稿は多対多で表現される。 + +| カラム | 意味 | | --- | --- | -| `post_id` | 対象投稿 | -| `version_no` | 投稿ごとの版番号。1 以上 | -| `event_type` | `create` / `update` / `discard` / `restore` | -| `title` | 当該版のタイトル | -| `url` | 当該版の URL | -| `thumbnail_base` | 当該版のサムネイル元 URL | -| `tags` | 当該版のタグ名スナップショット。空白区切り | -| `parent_id` | 当該版の親投稿 | -| `original_created_from` / `original_created_before` | 当該版の元コンテンツ作成日時範囲 | -| `created_at` | 版作成日時 | -| `created_by_user_id` | 版作成ユーザ。bot 操作は NULL | +| post_id | 子投稿 | +| parent_post_id | 親投稿 | -`PostVersion` は更新・削除不可の読み取り専用履歴として扱われる。`PostVersionRecorder` は投稿をロックし、直前版と同一スナップショットであれば `update` 版を増やさない。 +仕様: -### バックフィル仕様 +- 主キーは `(post_id, parent_post_id)`。 +- 自己参照は禁止。 +- 投稿作成・更新 API では `parent_post_ids` が必須。 +- `parent_post_ids` は空白区切りの ID 文字列として解釈される。 +- 存在しない親 ID が含まれると 422。 +- 自分自身を親にすると 422。 -`20260409123700_create_post_versions` では、既存の `post_tags` 履歴から投稿版を再構成する。 +この仕様は重要で、フロントや API クライアントは空でも `parent_post_ids` を送る必要がある。 -- 投稿ごとに `version_no = 1` の `create` 版を作る。 -- `post_tags.created_at` / `discarded_at` をイベントとして並べる。 -- 1 秒以内のタグ変更を同一バケットとしてまとめる。 -- 初回タグ付与が `posts.created_at + 1 second` 以内なら `create` 版へ統合する。 +## 投稿版 post_versions -これは過去履歴の近似復元であり、過去の投稿本文変更を完全に再現するものではない。 +投稿本体とタグ・親投稿構成のスナップショットは `post_versions` に保存される。 -## タグ `tags` とタグ名 `tag_names` +| 属性 | 意味 | +| --- | --- | +| post_id | 対象投稿 | +| version_no | 投稿内連番。1 以上 | +| event_type | create / update / discard / restore | +| title / url / thumbnail_base | 投稿本体スナップショット | +| tags | タグ名の空白区切りスナップショット | +| parent_post_ids | 親投稿 ID の空白区切りスナップショット | +| original_created_from / before | 元日時範囲 | +| created_by_user_id | 操作者 | -タグは 2 層構造である。 +`PostVersionRecorder` は更新前スナップショットを保証し、変更がなければ update 版を増やさない。 -- `tag_names`: 文字列としての名前。別名もここで扱う。 -- `tags`: カテゴリ・件数・実体としてのタグ。 +## タグ tags / tag_names -`tags.tag_name_id` は一意であり、実体タグは canonical な `tag_names` を参照する。 +タグ名文字列とタグ実体は分離されている。 + +- `tag_names`: 名前、別名関係、Wiki ページとの結合点。 +- `tags`: カテゴリ、投稿件数、タグ実体。 ### カテゴリ -| カテゴリ | 表示名 | 用途 | -| --- | --- | --- | -| `deerjikist` | ニジラー | 投稿者・人物・作者 | -| `meme` | 原作・ネタ元・ミーム等 | 元ネタ、文脈、シリーズ | -| `character` | キャラクター | キャラクタ | -| `general` | 一般 | 通常タグ | -| `material` | 素材 | 素材タグ | -| `meta` | メタタグ | 運用・状態タグ | -| `nico` | ニコニコタグ | 外部ニコニコタグ | +現行カテゴリは次である。 + +| category | 用途 | +| --- | --- | +| deerjikist | ニジラー | +| meme | ミーム・原作・ネタ元など | +| character | キャラクター | +| general | 一般 | +| material | 素材 | +| nico | ニコニコ外部タグ | +| meta | メタタグ | ### 別名 -`tag_names.canonical_id` が NULL のものは実体名、非 NULL のものは別名である。 - -制約は次の通り。 +`tag_names.canonical_id` により別名を表現する。 +- `canonical_id = NULL`: 正規名。 +- `canonical_id != NULL`: 別名。 - 別名の参照先も別名であってはならない。 -- 別名名には `:` を含められない。 -- タグまたは Wiki を持つ `tag_name` は別名化できない。 -- 検索・投稿正規化・補完では canonical 名へ解決する。 -- `/tags/name/:name` は exact match であり、別名解決 API ではない。 +- 別名名に `:` を含められない。 +- タグまたは Wiki ページを持つ tag_name は別名化できない。 -### タグ名サニタイズ +`TagName.canonicalise` は、既知の別名を正規名へ置換する。 -`tag_name_sanitisation_rules` は、タグ名を正規表現置換する内部ルールである。 +## タグサニタイズ tag_name_sanitisation_rules -- 主キーは `priority`。 -- `source_pattern` は正規表現。 -- `replacement` で置換する。 -- `TagName` バリデーションで `sanitise(name) == name` を要求する。 -- `apply!` は既存 `TagName` を一括変換し、衝突時はタグ統合または `TagName` 差し替えを行う。 +タグ名には優先度付きのサニタイズ規則がある。 -この機能は UI には出ていない。鯖缶向けの内部運用層である。 +| 属性 | 意味 | +| --- | --- | +| priority | 主キー。適用順序 | +| source_pattern | 正規表現 | +| replacement | 置換後文字列 | +| discarded_at | 論理削除 | + +`TagName` のバリデーションは、名前が `TagNameSanitisationRule.sanitise(name)` と一致することを要求する。 + +したがって、サニタイズ規則は「保存前に自動で直す」だけではなく、「規則に反する名前を拒否する」層でもある。 + +## タグ正規化 + +投稿作成・更新時、タグ入力は `Tag.normalise_tags!` で正規化される。 + +### カテゴリプレフィクス + +| 入力接頭辞 | category | +| --- | --- | +| general: / gen: | general | +| deerjikist: / djk: | deerjikist | +| meme: | meme | +| character: / chr: | character | +| material: / mtr: | material | +| meta: | meta | + +`nico:` は通常手入力では拒否される。 ### 自動付与タグ -`Tag.normalise_tags` は投稿作成・更新時に次を行う。 +通常の正規化では次が自動付与される。 | 条件 | 自動付与 | | --- | --- | -| `with_tagme: true` かつタグ数が 10 未満 | `タグ希望` | -| ニジラー系タグが 1 件もない | `ニジラー情報不詳` | +| タグ数が 10 未満 | タグ希望 | +| deerjikist カテゴリが無い | ニジラー情報不詳 | -投稿作成時は `with_tagme: true`。投稿更新時は `with_tagme: false` なので、更新時には `タグ希望` は自動付与されない。 +投稿更新時は `with_tagme: false` のため、更新時に「タグ希望」を新規補完しない。 -### ニコニコタグの扱い +### 親タグ展開 -通常投稿のタグ入力で `nico:` プレフィクスが含まれた場合、`Tag::NicoTagNormalisationError` が発生し、リクエストは `400 Bad Request` になる。 +`Tag.expand_parent_tags` により、入力タグの親タグを再帰的に追加する。 -`nico` カテゴリは手入力で直接追加するものではなく、ニコニコ同期・ニコタグ連携経路で扱う。 +結果として投稿には、明示タグだけではなく、親カテゴリ的なタグも保存される。 -## 投稿とタグの関係 `post_tags` +## 投稿タグ post_tags -`post_tags` は投稿とタグの多対多関係であり、同時にタグ付与履歴の基礎である。 +投稿とタグは多対多である。 -- `discarded_at` による論理削除。 -- `created_user_id` と `deleted_user_id` を保持。 -- 有効な `(post_id, tag_id)` は一意。 -- `PostTag#discard_by!` は `tags.post_count` を減算する。 -- 物理削除は禁止される。 +| 属性 | 意味 | +| --- | --- | +| post_id | 投稿 | +| tag_id | タグ | +| created_user_id | 付与者 | +| deleted_user_id | 削除者 | +| discarded_at | 論理削除時刻 | +| active_unique_key | 有効レコード一意制約用の生成列 | -現在の履歴表示の主役は `post_versions` だが、`post_tags` にもタグ単位のイベント履歴が残る。 +現行有効な `(post_id, tag_id)` の重複は DB レベルで防がれている。 -## 上位タグ `tag_implications` +`PostTag` は論理削除されるため、タグ付与履歴としても使われる。 -`tag_implications` は子タグと親タグの関係である。 +## タグ親子 tag_implications -- `tag_id`: 子タグ -- `parent_tag_id`: 親タグ -- `(tag_id, parent_tag_id)` は一意 -- 自己参照は禁止 +タグの親子関係は `tag_implications` で表す。 -投稿保存時には `Tag.expand_parent_tags` により、指定タグの親タグが再帰的に自動付与される。 +| カラム | 意味 | +| --- | --- | +| tag_id | 子タグ | +| parent_tag_id | 親タグ | -現行 UI では、投稿詳細サイドバーでタグを D&D することにより、上位タグ設定・解除・カテゴリ変更を行える。成功には admin 権限が必要である。 +仕様: -## ニコタグ連携 `nico_tag_relations` +- 同一組み合わせは一意。 +- admin のみ API で作成・削除できる。 +- nico カテゴリタグは親子操作不可。 +- 投稿タグ保存時、親タグは自動展開される。 -`nico_tag_relations` は外部ニコニコタグと内部タグの橋渡しである。 +## タグ版 tag_versions / nico_tag_versions -- `nico_tag_id` は `nico` カテゴリ必須。 -- `tag_id` は `nico` カテゴリ禁止。 -- `GET /tags/nico` でニコタグと連携先タグを取得する。 -- `PUT /tags/nico/:id` で連携先タグを更新する。 +通常タグと nico タグは別テーブルで履歴を持つ。 -ニコニコ同期では、新しく付いたニコタグに対してだけ、既存の連携先内部タグを自動付与する。 +### tag_versions -## ニジラー `deerjikists` +| 属性 | 意味 | +| --- | --- | +| tag_id | 対象タグ | +| version_no | タグ内連番 | +| event_type | create / update / discard / restore | +| name | 名前スナップショット | +| category | カテゴリ | +| aliases | 別名リスト | +| parent_tag_ids | 親タグ ID リスト | +| created_by_user_id | 操作者 | -`deerjikists` は外部プラットフォーム上の人物識別子と内部タグを対応させる。 +### nico_tag_versions -- 主キーは `(platform, code)` の複合主キー。 -- `platform` は `nico` / `youtube`。 -- 対応先タグは `deerjikist` カテゴリ必須。 +| 属性 | 意味 | +| --- | --- | +| tag_id | nico タグ | +| version_no | nico タグ内連番 | +| event_type | create / update / discard / restore | +| name | nico タグ名 | +| linked_tags | 連携内部タグ名 | +| created_by_user_id | 操作者 | -ニコニコ同期では、投稿者コードに対応する `deerjikist` があれば、そのタグを自動付与する。存在しない場合、既存内部タグにニジラー系タグが無ければ `ニジラー情報不詳` を付与する。 +## ニコタグ連携 nico_tag_relations + +`nico_tag_relations` は nico カテゴリタグと内部タグを結ぶ。 + +- `nico_tag_id`: nico カテゴリ必須。 +- `tag_id`: nico カテゴリ禁止。 + +これは外部ニコニコタグを内部タグ体系へ変換するための関係である。 + +## ニジラー deerjikists + +外部プラットフォーム上の人物識別子と内部タグを結ぶ。 + +| 属性 | 意味 | +| --- | --- | +| platform | nico / youtube | +| code | 外部識別子 | +| tag_id | deerjikist カテゴリのタグ | + +主キーは `(platform, code)`。 + +YouTube の `@handle` は、更新 API 内でチャンネル ID `UC...` へ正規化を試みる。 + +## 素材 materials + +現行実装で追加されている重要機能。 + +素材は、素材タグまたはキャラクタータグに紐づくファイル・参考 URL である。 + +重要なのは、`character` と `material` は単純な上下カテゴリではない点である。現行運用では、`character` タグを素材集合の代表タグとし、関連する `material` タグを `TagImplication` によって子タグとしてぶら下げる。つまり、キャラクタータグは「そのキャラクターに関する素材タグ群の入口」として機能する。 + +タグ分けするほどではないが異なる素材差分については、将来的に **包摂素材** として単一 Material の配下へ複数保持できるようにする計画である。これは、立ち絵差分・音声差分・参考画像などを何でもタグ化してタグ空間を肥大化させるのを避けるための拡張である。 + + + +| 属性 | 意味 | +| --- | --- | +| url | 参考 URL。論理削除されていない場合は一意 | +| parent_id | 親素材。将来的な包摂素材表現の土台になる | +| tag_id | 対応タグ。素材 1 件につき一意 | +| created_by_user_id | 作成者 | +| updated_by_user_id | 更新者 | +| discarded_at | 論理削除 | +| file | Active Storage 添付 | + +### 制約 + +- `tag_id` は必須かつ一意。 +- タグは `material` または `character` カテゴリのみ許可。 +- `character` は素材集合の代表タグとして許可される。 +- `material` は具体的な素材タグとして許可される。 +- `url` または `file` の少なくとも一方が必須。 +- `discarded_at` が NULL の場合だけ `url` 一意制約が効く。 + +### 現行運用上の意味 + +- キャラクターに関連する複数素材は、現時点では `TagImplication` によって `character` 親タグと `material` 子タグに分解して扱う。 +- そのため、`materials.tag_id` が一意であっても、素材をタグ単位へ分ける限りは大きな問題は起きにくい。 +- ただし、タグを増やすほどではない素材差分を複数持ちたい場合は、現行設計だけでは表現力が足りない。 +- この不足分を埋める将来仕様が **包摂素材** である。 + +### バージョン + +`material_versions` が存在し、素材の URL・親素材・タグ・作成/更新者・discard 状態を履歴化する構造がある。 + +ただし、コントローラ上は素材の version API は確認できない。モデル・サービス側の実運用接続は追加確認が必要。 ## Wiki -Wiki は、ページ本体と改訂履歴を DB 内で持つ版管理システムである。 +Wiki はタグ名に紐づく説明ページである。 -### `wiki_pages` +### wiki_pages -- `tag_name_id` がタイトルに対応する。 -- 1 タイトルにつき 1 ページ。 -- `created_user_id` / `updated_user_id` を持つ。 -- `next_asset_no` を持つが、現行の画像アップロード UI/API は未接続。 - -### `wiki_revisions` - -- `kind` は `content` または `redirect`。 -- `base_revision_id` で改訂の親を持つ。 -- `message` を持てる。 -- `lines_count` と `tree_sha256` を持つ。 -- `redirect` の場合は `redirect_page_id` を持つ。 - -### 行ストレージ - -- 本文は `wiki_revision_lines` を介して行単位で構成される。 -- 行本文は `wiki_lines` に SHA-256 付きで保存される。 -- 同じ行は重複排除される。 - -これは単なる Markdown 本文 1 カラム保存ではない。DB 上に Git 風の行ストアを持つ構造である。 - -## Wiki 画像 `wiki_assets` - -`wiki_assets` テーブルは存在する。 - -| 属性 | 概要 | +| 属性 | 意味 | | --- | --- | -| `wiki_page_id` | 対象ページ | -| `no` | ページ内画像番号 | -| `alt_text` | 代替テキスト | -| `sha256` | ファイル重複判定用 SHA-256 | -| `created_by_user_id` | 作成者 | +| tag_name_id | Wiki タイトルに対応する tag_name | +| body | 現行本文キャッシュ | +| created_user_id | 作成者 | +| updated_user_id | 更新者 | +| next_asset_no | Wiki 画像/添付素材の採番用 | +| discarded_at | 論理削除 | -ただし、2026-04-19 時点では `WikiAsset` モデル、controller、route、フロント実装が確認できない。したがって **スキーマ準備済み、機能未接続** と扱う。 +`wiki_pages.tag_name_id` は一意。 -2026-04-20 の開発者ヒアリングでは、Wiki 画像は通常 URL ではなく **専用記法** で扱う方針になった。候補は `` のように、ページ内画像番号または asset 識別子を Markdown の URL 部へ入れる方式である。実装時は、Markdown レンダラ側で `asset:` スキームを検出し、対象ページの `wiki_assets.no` から Active Storage URL へ解決する。 +### wiki_revisions / wiki_lines -## 素材 `materials` +Wiki は改訂と行ストアを分離する。 -`materials` は素材集の実体である。 +- `wiki_revisions`: 改訂メタ情報。 +- `wiki_revision_lines`: 改訂内の行順序。 +- `wiki_lines`: 行本文を SHA-256 で重複排除保存。 -| 属性 | 概要 | -| --- | --- | -| `url` | 参考 URL。NULL 可 | -| `file` | Active Storage 添付。画像・動画・音声を主に想定 | -| `tag_id` | 対応タグ。現行モデルでは一意 | -| `parent_id` | 親素材 | -| `created_by_user_id` / `updated_by_user_id` | 作成・更新ユーザ | -| `discarded_at` | 論理削除 | -| `active_url` | 未削除時のみ URL を返す生成列。一意制約あり | +`wiki_revisions.kind` は現行 enum で `content` / `redirect` を持つ。ただし `Wiki::Commit#redirect!` は現在 `廃止しました.` として例外を投げる。 -`Material` は `url` または `file` のどちらかを必須とする。タグは `character` または `material` カテゴリを許容する。これは単なる実装の緩さではなく、`character` を「下位区分としての material を包括する代表素材タグ」として扱う設計判断である。したがって、素材タグとしては `material` が基本だが、代表素材・キャラクタ素材の受け皿として `character` も許容する。 +### wiki_versions -`material_versions` テーブルは存在するが、モデル・履歴記録・API・画面は未接続である。 +`wiki_versions` も存在し、Wiki ページの版管理スナップショットを保持する。 -## 閲覧済 `user_post_views` +現行 `Wiki::Commit.content!` は `WikiVersionRecorder.record!` を呼ぶため、行単位 revision と別に、通常の version 履歴も記録される。 -ユーザ単位で投稿の閲覧済状態を管理する。 +### Wiki 競合制御 + +`Wiki::Commit.content!` は `base_revision_id` を受け取る。指定があり、現在の最大 revision id と一致しない場合は `Wiki::Commit::Conflict` を投げ、コントローラは 409 を返す。 + +現行 `WikiPagesController#update` は `params[:base_revision_id]` を使う。したがって、過去仕様書にあった「クライアント送信値を使わないため競合検出が活きていない」という記述は、現行実装では修正済みである。 + +## 閲覧済 user_post_views + +ユーザごとの投稿閲覧済み状態を保持する。 - 主キーは `(user_id, post_id)`。 -- 投稿詳細画面でボタンにより付与・解除できる。 -- 自動既読化の設定は将来構想としてあるが、現行は未接続。 +- `POST /posts/:id/viewed` で付与。 +- `DELETE /posts/:id/viewed` で解除。 -## 類似度 `post_similarities` / `tag_similarities` +## 類似度 post_similarities / tag_similarities -投稿・タグには類似度の事前計算テーブルがある。 +投稿類似度とタグ類似度は事前計算テーブルで保持される。 -- `post_similarities`: 投稿に付いたタグ集合から投稿同士の cosine 類似度を計算。 -- `tag_similarities`: タグに属する投稿集合からタグ同士の cosine 類似度を計算。 -- それぞれ上位 20 件を保存する。 - -投稿詳細では `post.related(limit: 20)` により関連投稿を返す。タグ類似度については、テーブル・モデル・タスクはあるが、現行 UI での主導線は薄い。 - -## 上映会 `theatres` - -Theatre は共同視聴用の会場ドメインである。 - -| テーブル | 役割 | -| --- | --- | -| `theatres` | 会場、現在上映中投稿、ホスト、期間 | -| `theatre_comments` | 会場コメント。主キーは `(theatre_id, no)` | -| `theatre_watching_users` | 在席ユーザ。`expires_at` で 30 秒 TTL | - -現行では会場一覧・作成・編集は存在しない。トップメニューは固定で `/theatres/1` を指す。 - -# 現行機能仕様 - -## 投稿一覧・検索 - -### API - -`GET /posts` - -### パラメータ - -| パラメータ | 意味 | -| --- | --- | -| `tags` | 空白区切りタグ列 | -| `match` | `all` または `any` | -| `title` | タイトル部分一致 | -| `url` | URL 部分一致 | -| `original_created_from` / `original_created_to` | 元コンテンツ作成日時範囲 | -| `created_from` / `created_to` | 投稿作成日時範囲 | -| `updated_from` / `updated_to` | 投稿更新日時範囲 | -| `page` / `limit` | ページング | -| `order` | `field:direction` | - -`order` の field は次の 5 種である。 - -- `title` -- `url` -- `original_created_at` -- `created_at` -- `updated_at` - -`title` と `url` は direction 未指定時に昇順、それ以外は降順である。 - -### タグ検索 - -タグ検索は次の意味を持つ。 - -| 指定 | 意味 | -| --- | --- | -| `match=all` または未指定 | 各タグを AND 的に絞り込む | -| `match=any` | 各タグ条件を OR 的に結合する | -| `not:タグ名` | 否定条件 | -| 別名タグ | 検索前に canonical 名へ解決される | - -`match=any` では否定条件も OR の一部として扱われる。つまり `A not:B` は「A が付いている、または B が付いていない」に近い挙動になる。ここは直感とずれやすい。 - -### 更新日時 - -一覧返却の `updated_at` は、単純な `posts.updated_at` ではない。 - -サーバは次の大きい方を `updated_at_all` として返し、並び替えにも使う。 - -- `posts.updated_at` -- 対象投稿に紐づく `post_tags.updated_at` の最大値 - -タグ変更も投稿更新として扱う設計である。 - -### 返却 - -返却は `{ posts, count }`。各投稿にはタグ・投稿者・サムネイル URL が含まれる。 - -## ランダム投稿 - -### API - -`GET /posts/random` - -`tags` と `match` を受け取り、絞り込み結果から `RAND()` で 1 件返す。 - -## 投稿詳細 - -### API - -`GET /posts/:id` - -### 返却 - -- 投稿基本情報 -- タグツリー -- 関連投稿 最大 20 件 -- 閲覧済フラグ - -タグは `tag_implications` に基づきツリー化される。ループがあっても再帰停止する。 - -### 画面 - -投稿詳細画面では次が表示される。 - -- 埋め込み表示 -- タグツリー -- 投稿情報サイドバー -- 関連投稿 -- 編集タブ -- 閲覧済トグル -- 履歴リンク - -投稿詳細サイドバーでは、D&D により上位タグ設定・解除・カテゴリ変更を試みる。API 側で admin 権限が要求されるため、member / guest は失敗する。 - -## 投稿埋め込み - -`PostEmbed` の現行専用対応は次の通り。 - -| サイト | 条件 | 表示 | +| テーブル | 単位 | 特徴量 | | --- | --- | --- | -| ニコニコ動画 | `nicovideo.jp/watch/:id` | `NicoViewer` | -| YouTube | `youtube.com` かつ `v` パラメータあり | `react-youtube` | -| X / Twitter | `x.com` または `twitter.com` の `/status/:id` | `TwitterEmbed` | -| その他 | ユーザ確認後 | `iframe` | +| post_similarities | 投稿 → 関連投稿 | 投稿に付いたタグ集合 | +| tag_similarities | タグ → 関連タグ | タグに属する投稿集合 | -`youtu.be`、Pixiv、Bluesky、Tiktok、bilibili、ニジカ投稿局などは専用埋め込み未実装である。 +上位 20 件のみ保存する設計。 -## 新規投稿 +## 上映会 theatres -### API +上映会は共同視聴機能である。 -`POST /posts` - -### 権限 - -member 以上。 - -### 入力 - -- `title` -- `url` -- `thumbnail` -- `tags` -- `original_created_from` -- `original_created_before` - -### 保存フロー - -1. 投稿レコード作成。 -2. サムネイル添付。 -3. タグ名正規化。 -4. 親タグ展開。 -5. `post_tags` 同期。 -6. サムネイルを 180x180 へリサイズ。 -7. `post_versions` に `create` 版を記録。 - -`nico:` タグが含まれる場合は `400 Bad Request`。 - -## 投稿編集 - -### API - -`PUT /posts/:id` - -### 権限 - -member 以上。 - -### 更新対象 - -- `title` -- `tags` -- `original_created_from` -- `original_created_before` - -URL とサムネイルの通常編集は現行 API では扱っていない。 - -既存の `nico` カテゴリタグは維持され、一般タグ側だけが再正規化される。その後、親タグ展開と `post_tags` 同期を行い、差分があれば `post_versions` に `update` 版を記録する。 - -### 排他制御 - -現行 API には投稿編集の排他制御がない。issue では `updated_at` をリクエストに含め、差があれば 409 を返す構想があるが、未実装である。 - -## 閲覧済 - -| API | 意味 | +| 属性 | 意味 | | --- | --- | -| `POST /posts/:id/viewed` | 閲覧済にする | -| `DELETE /posts/:id/viewed` | 未閲覧に戻す | +| name | 会場名 | +| opens_at / closes_at | 開始/終了時刻 | +| kind | 種別。現行 UI では薄い | +| current_post_id | 現在上映中投稿 | +| current_post_started_at | 現在投稿の開始時刻 | +| next_comment_no | コメント採番 | +| host_user_id | 現在ホスト | +| created_by_user_id | 作成者 | +| discarded_at | 論理削除 | -認証済ユーザなら操作可能である。 +# API 仕様 -## 投稿版履歴 +## 投稿 API -### API +### GET /posts -`GET /posts/versions` +投稿一覧を取得する。 -### パラメータ +#### パラメータ | パラメータ | 意味 | | --- | --- | -| `post` | 投稿 ID で絞り込み | -| `tag` | タグ ID で絞り込み | -| `page` / `limit` | ページング | +| tags | 空白区切りタグ検索 | +| match | `all` 相当または `any` | +| title | タイトル部分一致 | +| url | URL 部分一致 | +| original_created_from / original_created_to | 元コンテンツ作成日時範囲 | +| created_from / created_to | 投稿作成日時範囲 | +| updated_from / updated_to | 投稿またはタグ更新日時範囲 | +| page | ページ。既定 1 | +| limit | 件数。既定 20 | +| order | `field:direction` | -タグで絞り込む場合、現在版または直前版のタグスナップショットに対象タグ名が含まれる版を返す。これにより、タグが追加された版だけでなく、削除された版も拾える。 +`order` の field は次。 -### 返却 +- title +- url +- original_created_at +- created_at +- updated_at -返却は `{ versions, count }`。 +`updated_at` は `posts.updated_at` と `post_tags.updated_at` の最大値を `updated_at_all` として使う。 -各 version は次を含む。 +### タグ検索意味論 -- `post_id` -- `version_no` -- `event_type` -- `title.current` / `title.prev` -- `url.current` / `url.prev` -- `thumbnail.current` / `thumbnail.prev` -- `thumbnail_base.current` / `thumbnail_base.prev` -- `tags[]`: `context` / `added` / `removed` -- `original_created_from.current` / `.prev` -- `original_created_before.current` / `.prev` -- `created_at` -- `created_by_user` +`tags` は空白区切り。各要素は `TagName.canonicalise` により別名解決される。 -現行では `thumbnail.current` / `prev` は常に NULL で、サムネイル表示は `thumbnail_base` に依存する。 +- `match=any`: OR。 +- それ以外: AND。 +- `not:` 接頭辞: 否定条件。 -### 画面 +注意: `match=any` で `not:` を混ぜると、SQL 的には「否定条件を OR する」ため、期待とズレる可能性がある。仕様として許すか、UI で制限するか要確認。 -フロントの `/posts/changes` は、内部的には `GET /posts/versions` を呼ぶ。つまり画面名は「耕作履歴」だが、主役データはタグイベントではなく投稿版スナップショットである。 +### GET /posts/random -### 復元 +現在の `tags` / `match` 条件に合う投稿をランダムに 1 件返す。 -履歴画面の「復元」は専用の restore API ではない。選択版の `title`、`tags`、`original_created_from`、`original_created_before` を `PUT /posts/:id` へ送る。 +### GET /posts/:id -そのため、以下の制限がある。 +投稿詳細を取得する。 -- `url` は復元されない。 -- `thumbnail_base` は現時点では復元されない。将来的に入力欄を追加した段階で復元対象へ含める方針である。 -- `event_type = restore` の版は現行フローでは作られない。 -- `nico:` タグは復元送信時に除外される。 +返却には次を含む。 -2026-04-20 の開発者ヒアリングでは、`thumbnail_base` は将来的に復元対象へ含める方針とされた。ただし現行フロントの編集入力欄に出てこない項目であるため、現段階では復元しない。これは「履歴として保持しない」のではなく、「編集 UI が責任を持てる項目だけ戻す」という暫定仕様である。 +- 投稿基本情報。 +- タグ木構造。 +- 関連投稿最大 20 件。 +- 現在ユーザの閲覧済みフラグ。 +- 親投稿・子投稿・兄弟投稿情報。 -## 旧タグ変更履歴 +### POST /posts -### API +投稿を作成する。member 以上必須。 -`GET /posts/changes` +入力: -この API は `post_tags` から `add` / `remove` のイベント列を生成する。現行画面の主導線ではなくなっているが、API としては残っている。 - -`/posts/versions` と `/posts/changes` は意味が違う。2026-04-20 の開発者ヒアリングでは、`/posts/changes` は将来的に廃止する可能性が高いと整理された。現時点では後方互換上残っている旧 API として扱う。 - -# タグ機能 - -## タグ一覧・検索 - -### API - -`GET /tags` - -### パラメータ - -| パラメータ | 意味 | -| --- | --- | -| `post` | 特定投稿に付いたタグだけへ絞る | -| `name` | 名前部分一致 | -| `category` | カテゴリ一致 | -| `post_count_gte` / `post_count_lte` | 投稿件数範囲 | -| `created_from` / `created_to` | 作成日時範囲 | -| `updated_from` / `updated_to` | 更新日時範囲 | -| `page` / `limit` | ページング | -| `order` | `field:direction` | - -`order` の field は次の 5 種である。 - -- `name` -- `category` -- `post_count` -- `created_at` -- `updated_at` - -カテゴリ並びは独自順序である。 - -1. `deerjikist` -2. `meme` -3. `character` -4. `general` -5. `material` -6. `meta` -7. `nico` - -## タグ補完 - -### API - -`GET /tags/autocomplete` - -### パラメータ - -| パラメータ | デフォルト | 意味 | +| パラメータ | 必須 | 意味 | | --- | --- | --- | -| `q` | なし | 検索文字列 | -| `nico` | `true` | `nico:` 側候補を含める | -| `present` | `true` | `post_count > 0` のタグに絞る | +| title | 任意 | タイトル | +| url | 必須 | URL | +| thumbnail | 任意 | サムネイルファイル | +| tags | 任意 | 空白区切りタグ | +| original_created_from | 任意 | 元日時下限 | +| original_created_before | 任意 | 元日時上限 | +| parent_post_ids | 必須 | 空白区切り親投稿 ID | -### 挙動 +保存フロー: -- `q` 先頭の `not:` は除去して補完する。 -- canonical 名の前方一致を拾う。 -- 別名の前方一致も拾い、canonical 側のタグを返す。 -- 別名ヒット時は `matched_alias` を付ける。 -- 並びは `post_count DESC, tag_names.name`。 +1. 投稿を保存。 +2. タグを正規化。 +3. 関連タグのバージョンスナップショットを確保。 +4. 親タグを展開。 +5. `post_tags` を同期。 +6. 親投稿関係を同期。 +7. サムネイルをリサイズ。 +8. `post_versions` に create 版を記録。 + +### PUT /posts/:id + +投稿を更新する。member 以上必須。 + +更新対象: + +- title +- original_created_from +- original_created_before +- tags +- parent_post_ids + +注意: + +- URL の更新は現行 API では行わない。 +- nico カテゴリタグは既存分を維持したまま、手入力側タグを再計算する。 +- `parent_post_ids` は更新時も必須。 +- 投稿の optimistic locking 用 `version_no` は posts テーブルにまだ無い。履歴はあるが、投稿更新 API で base version を照合する仕様は未実装。 + +### POST /posts/:id/viewed / DELETE /posts/:id/viewed + +ログイン済みユーザの閲覧済み状態を付与・解除する。 + +### GET /posts/changes + +投稿タグ付与履歴を取得する。 + +| パラメータ | 意味 | +| --- | --- | +| id | 投稿 ID 絞り込み | +| tag | タグ ID 絞り込み | +| page / limit | ページング | + +返却されるのは、投稿本体履歴ではなく `post_tags` の add/remove イベントである。 + +### GET /posts/versions + +投稿本体スナップショット履歴を取得する。 + +| パラメータ | 意味 | +| --- | --- | +| post | 投稿 ID 絞り込み | +| tag | タグ ID 絞り込み | +| page / limit | ページング | + +前版との差分として、title / url / thumbnail_base / tags / parent_post_ids / original_created_from / original_created_before などを返す。 + +## タグ API + +### GET /tags + +タグ一覧・検索を取得する。 + +| パラメータ | 意味 | +| --- | --- | +| post | 指定投稿に付いたタグだけに絞る | +| name | 名前部分一致 | +| category | カテゴリ一致 | +| post_count_gte / post_count_lte | 投稿件数範囲 | +| created_from / created_to | 作成日時範囲 | +| updated_from / updated_to | 更新日時範囲 | +| page / limit | ページング | +| order | `field:direction` | + +`order` の field は name / category / post_count / created_at / updated_at。 + +category 並びは独自順序。 + +1. deerjikist +2. meme +3. character +4. general +5. material +6. meta +7. nico + +注意: `count` は `q.size` を返しており、ページング後/前の扱いが ActiveRecord の状態に依存しうる。厳密な総件数仕様としては弱い。 + +### GET /tags/with-depth + +階層表示用タグを取得する。 + +| パラメータ | 意味 | +| --- | --- | +| parent | 親タグ ID。無ければルートタグ | + +対象カテゴリは `meme` / `character` / `material` に限定される。 + +返却タグには `has_children` が付く。 + +### GET /tags/autocomplete + +タグ補完。 + +| パラメータ | 既定 | 意味 | +| --- | --- | --- | +| q | 空 | 前方一致検索語 | +| nico | true | nico 候補を含める | +| present | true | post_count > 0 に限定 | + +挙動: + +- `q` 先頭の `not:` は除去する。 +- canonical 名前方一致を検索する。 +- 別名前方一致も拾い、正規タグを返す。 +- 別名ヒット時は `matched_alias` を返す。 - 最大 20 件。 -## タグ詳細 +### GET /tags/:id / GET /tags/name/:name -| API | 意味 | -| --- | --- | -| `GET /tags/:id` | ID でタグ取得 | -| `GET /tags/name/:name` | 名前完全一致でタグ取得 | -| `GET /tags/:id/deerjikists` | タグに紐づくニジラー一覧 | -| `GET /tags/name/:name/deerjikists` | 名前指定版 | -| `GET /tags/name/:name/materials` | 素材ツリー取得 | +タグ詳細を取得する。 -`/tags/name/:name` は exact match であり、別名解決をしない。 +`/tags/name/:name` は exact name であり、別名から正規タグへ自動解決しない。 -## タグ階層取得 +### PUT /tags/:id -### API +タグ全体更新。member 以上必須。 -`GET /tags/with-depth` +現行実装では以下を受ける。 -### パラメータ +- name +- category +- aliases +- parents + +仕様: + +- nico タグの編集は禁止。 +- `nico` カテゴリへの変更は禁止。 +- 特殊タグの改名は禁止。 +- name 変更時は、対応 Wiki があれば Wiki 版も更新記録される。 +- aliases 更新時は対象・影響タグの snapshot を確保する。 +- parents 更新時は親タグを再正規化し、既存親関係を全置換する。 + +### PATCH /tags/:id + +狭いタグ更新。member 以上必須。詳細は `update_all` と分担があり、実装確認継続対象。 + +### GET /tags/versions + +タグ履歴を取得する。 | パラメータ | 意味 | | --- | --- | -| `parent` | 親タグ ID。未指定ならルートタグを返す | +| id | タグ ID 絞り込み | +| page / limit | ページング | -### 返却 +前版との差分として、name / category / aliases / parent_tags を返す。 -対象カテゴリは `meme` / `character` / `material`。各タグに `has_children` と `children: []` を付けて返す。 +### POST /tags/:parent_id/children/:child_id -現行では素材サイドバーで使用される。`meme` は子を持つものだけ表示される。 +タグ親子関係を追加する。admin のみ。 -## タグ更新 +- nico タグは不可。 +- 子タグの snapshot を確保してから関係追加。 +- tag_versions に update を記録。 -### API +### DELETE /tags/:parent_id/children/:child_id -`PUT /tags/:id` +タグ親子関係を削除する。admin のみ。 -### 権限 +## ニコタグ API -member 以上。 +### GET /tags/nico -### 更新対象 - -- `name` -- `category` - -タグ名変更は `tag.tag_name.update!`、カテゴリ変更は `tag.update!` で行われる。 - -## 上位タグ管理 - -| API | 意味 | 権限 | -| --- | --- | --- | -| `POST /tags/:parent_id/children/:child_id` | child を parent の子にする | admin | -| `DELETE /tags/:parent_id/children/:child_id` | child を parent の子から外す | admin | - -現行フロントでは、投稿詳細サイドバーの D&D 操作からこれらを叩く。 - -ただし、専用の「上位タグ設定画面」は未実装である。 - -## 別名タグ管理 - -モデル・検索・補完・正規化の土台はあるが、別名の作成・解除・一覧管理 UI/API は未実装である。 - -issue では「管理者用別名タグ作成画面」が残っている。公開前にここはかなり重要である。 - -# ニコニコ連携 - -## ニコタグ一覧 - -### API - -`GET /tags/nico` - -### 挙動 - -- `Tag.nico_tags` を `updated_at DESC` で取得する。 -- `cursor` に ISO8601 時刻を渡すと、その時刻より古いものを取得する。 -- `limit` デフォルトは 20。 -- 各ニコタグに `linked_tags` を付与して返す。 - -## ニコタグ更新 - -### API - -`PUT /tags/nico/:id` - -### 権限 - -member 以上。 - -### 挙動 - -- 対象タグは `nico` カテゴリ必須。 -- 入力 `tags` を通常タグとして再正規化する。 -- 連携先に `nico` カテゴリは指定不可。 -- 成功時、連携先タグ配列を返す。 - -## 日次同期 `nico:sync` - -現行タスクは外部 Python スクリプト `get_videos.py` を呼び、ニコニコ動画情報を取り込む。 - -### 主な流れ - -1. `MYSQL_USER`、`MYSQL_PASS`、`NIZIKA_NICO_PATH` を環境変数から読む。 -2. `python3 #{NIZIKA_NICO_PATH}/get_videos.py` を実行する。 -3. 既存投稿は `nicovideo.jp/watch/:code` の正規表現で検索する。 -4. タイトル、元投稿日、サムネイルを更新する。 -5. 新規投稿なら `タグ希望`、`bot操作`、`ニコニコ`、`動画` を初期付与する。 -6. 外部タグを `nico:` 付き内部タグへ変換する。 -7. 新しく追加されたニコタグについてだけ、連携済内部タグを展開する。 -8. deerjikist 対応があればニジラータグを付与する。 -9. 必要に応じて `bot操作` を追加する。 -10. 作成・変更があれば `PostVersionRecorder` で投稿版を記録する。 - -これは単なるミラーではなく、外部タグを内部知識へ変換する ETL である。 - -# Wiki 機能 - -## 一覧・検索 - -| API | 意味 | -| --- | --- | -| `GET /wiki` | Wiki 一覧またはタイトル検索 | -| `GET /wiki/search` | `index` への委譲 | - -`title` 未指定なら全件、指定時は `tag_names.name LIKE %title%` で最大 20 件を返す。 - -フロントの `/wiki` は検索画面であり、本文検索欄に見える UI があっても、バックエンドは本文を検索していない。現行の Wiki 検索はタイトル検索のみである。 - -## 詳細表示 - -| API | 意味 | -| --- | --- | -| `GET /wiki/:id` | ID 指定 | -| `GET /wiki/title/:title` | タイトル完全一致 | - -`version` パラメータがある場合は指定改訂を表示する。なければ最新改訂を表示する。 - -返却には次が含まれる。 - -- `id` -- `title` -- `body` -- `revision_id` -- `pred` -- `succ` -- `updated_at` - -## リダイレクト - -改訂が `redirect` の場合、サーバはリダイレクト先タイトルへ `301 Moved Permanently` で遷移させる。 - -ただし現行 UI にはリダイレクト作成・編集導線がない。 - -## 存在確認 - -| API | 成功時 | 失敗時 | -| --- | --- | --- | -| `GET /wiki/:id/exists` | 204 | 404 | -| `GET /wiki/title/:title/exists` | 204 | 404 | - -## 差分表示 - -### API - -`GET /wiki/:id/diff?from=...&to=...` - -### 挙動 - -- `from` 省略時は空本文相当。 -- `to` 省略時は現行改訂。 -- `content` 改訂同士のみ比較可。 -- 結果は `context` / `added` / `removed` の列で返す。 - -## 作成 - -### API - -`POST /wiki` - -### 権限 - -member 以上。 - -### 入力 - -- `title` -- `body` -- `message` 任意 - -タイトルに対応する `TagName` を作成または再利用し、`Wiki::Commit.content!` で初回改訂を作る。 - -## 更新 - -### API - -`PUT /wiki/:id` - -### 権限 - -member 以上。 - -### 挙動 - -- `title` と `body` は必須。 -- タイトル変更は受け付けない。変更されていれば 422。 -- `message` は受け取れる。 -- `Wiki::Commit.content!` で改訂を作る。 - -### 競合制御の実態 - -`Wiki::Commit` は `base_revision_id` による競合検出を持つ。しかし `WikiPagesController#update` はクライアントから受けた基底版ではなく、更新直前にサーバ側で `page.current_revision.id` を読み直して使う。 - -つまり、衝突検出器はあるが、現行 API はクライアント起点の競合検出を十分に活かしていない。 - -## 履歴 - -### API - -`GET /wiki/changes` - -### パラメータ +nico カテゴリタグ一覧を取得する。 | パラメータ | 意味 | | --- | --- | -| `id` | Wiki ページ ID。未指定なら全体履歴 | +| limit | 件数。既定 20 | +| cursor | ISO8601 時刻。`updated_at < cursor` | -最大 200 件の `WikiRevision` を返す。`pred` は返るが、`succ` は常に NULL である。 +`linked_tags` を含む。 -# 素材集 +### PUT /tags/nico/:id -## 位置づけ +nico タグと内部タグの連携を更新する。member 以上必須。 -素材集は、タグに紐づく画像・動画・音声・参考 URL を管理する機能である。 +- 対象タグは nico カテゴリ必須。 +- 入力 `tags` を通常タグとして正規化する。 +- 連携先に nico カテゴリが含まれると 400。 +- `nico_tag_versions` に update が記録される。 -現行ではルートと画面は存在するが、トップナビでは `素材` メニューが `visible: false` になっている。つまり機能は実装され始めているが、正式導線としてはまだ隠し気味である。 +## ニジラー API -## 画面 +### GET /deerjikists/:platform/:code -| パス | 概要 | -| --- | --- | -| `/materials` | 素材一覧/タグ別素材ツリー | -| `/materials/new` | 素材追加 | -| `/materials/:id` | 素材詳細・編集 | -| `/materials/search` | import はあるがルートはコメントアウト | +外部識別子からニジラー対応タグを取得する。 -素材一覧は左サイドバーのタグ階層から対象タグを選び、対応する素材と子タグの素材を表示する。 +### PUT /deerjikists/:platform/:code -## API +ニジラー対応を作成・更新する。member 以上必須。 -| API | 権限 | 意味 | -| --- | --- | --- | -| `GET /materials` | 認証不要 | 素材一覧 | -| `GET /materials/:id` | 認証不要 | 素材詳細 | -| `POST /materials` | 認証済み、guest 可 | 素材作成 | -| `PUT /materials/:id` | member 以上 | 素材更新 | -| `DELETE /materials/:id` | member 以上 | 論理削除 | +入力: -## 一覧 +- tag_id -`GET /materials` は次を受け付ける。 +### DELETE /deerjikists/:platform/:code + +ニジラー対応を削除する。member 以上必須。 + +## 素材 API + +### GET /materials + +素材一覧を取得する。 | パラメータ | 意味 | | --- | --- | -| `page` / `limit` | ページング | -| `tag_id` | タグで絞り込み | -| `parent_id` | 親素材で絞り込み | +| page / limit | ページング | +| tag_id | 紐づくタグで絞る | +| parent_id | 親素材で絞る | -作成日時降順、ID 降順で返す。 +返却は `materials` と `count`。 -## 詳細 +### GET /materials/:id -`GET /materials/:id` は素材情報に加え、対応タグの Wiki 本文を `wiki_page_body` として返す。 +素材詳細を取得する。 -## 作成・更新 +返却には素材本体に加え、素材タグに対応する Wiki 本文 `wiki_page_body` が含まれる。 -### 作成 +### POST /materials -`POST /materials` は認証済みなら可能で、member 権限を要求しない。BTRC Hub では初回利用時に guest ユーザが自動発行されるため、公開時も guest に素材作成を許可する仕様とする。 +素材を作成する。現行実装ではログイン必須。 -入力は次の通り。 +計画仕様としては **member 以上に制限する**。当初は guest 作成可能の意図があったが、ファイルアップロードを伴うため、オブジェクトストレージへの大量投入・違法ファイル混入・容量爆撃のリスクを避ける。 -- `tag` -- `file` -- `url` +入力: -`tag` は必須。`file` または `url` のどちらかは必須。対象タグがなければ作成し、カテゴリは `material` になる。 +| パラメータ | 必須 | 意味 | +| --- | --- | --- | +| tag | 必須 | 対応タグ名 | +| file | file または url の一方必須 | 素材ファイル | +| url | file または url の一方必須 | 参考 URL | -### 更新 +タグが存在しない場合は `material` カテゴリで作成される。 -`PUT /materials/:id` は member 以上。 +### PUT /materials/:id -`tag` と、`file` または `url` が必須。`file` が無い状態で更新すると既存添付ファイルは purge される。 +素材を更新する。member 以上必須。 -## 現行の弱点 +- tag +- file +- url -- `material_versions` は未接続。 -- 素材追加は認証済み guest でも可能であり、公開時には再検討が必要。 -- ファイル種別の厳密なサーバ側制限は薄い。 -- タグ一意のため、1 タグに複数素材を持つ設計ではない。 +file が渡されない場合は既存ファイルを purge する。 -# プレビュー取得 +### DELETE /materials/:id -## API +素材を論理削除する。member 以上必須。 -| API | 意味 | +## Wiki API + +### GET /wiki + +Wiki 一覧・タイトル検索。 + +| パラメータ | 意味 | | --- | --- | -| `GET /preview/title` | 対象 URL の `