BluePeriod Docs
開発

Publish System

BluePeriodのPublish(公開・出版)システムのアーキテクチャ、状態管理、データフロー、多言語対応を網羅的に解説

Publish System (22_publish_system.md)

1. 概要

Publishシステムは、ユーザーが執筆した作品をBluePeriodプラットフォーム上で公開するための一連のフローを提供します。ウィザード形式の4タブUI(Metadata → Localization → Quality → Final)により、メタデータ入力、多言語ローカライズ、AI品質レビュー、最終確認を段階的に進めます。

主な役割:

  • 作品メタデータ(タイトル、見出し、あらすじ、タグ、表紙画像)の入力とAI生成
  • 多言語ローカライゼーション(メタデータ翻訳 + プロット構造翻訳)
  • AI品質レビューによる公開可否判定
  • Supabase Storage / Databaseへのコンテンツ保存
  • 公開後のメタデータ編集・非公開化

2. 画面構成とタブ構造

2.1. ルーティング

Publish画面は /project/[id]/publish に配置されています。

  • Server Component: 認証チェック、既存公開データの取得
  • Client Component: タブ切り替え、状態管理、ウィザードUI

2.2. タブ構成

タブTabId役割完了条件
Metadatametadata表紙画像、タイトル、見出し、あらすじ、タグの入力。AI生成機能あり原語のtitleとsynopsisが空でない
Localizationlocalization多言語メタデータとプロット構造の翻訳原語が英語の場合は自動完了。それ以外は英語翻訳のtitle/synopsisが空でない
QualityqualityAIによる作品品質レビューレビュー結果でis_publishableがtrue
Finalfinal公開先選択(新規/既存スロット)、最終プレビュー、公開実行上記3タブがすべて完了

タブステータスは getTabStatus() で判定され、UI上に ✅(complete)/ ⚠(incomplete)として表示されます。

2.3. タブコンポーネント

MetadataTab

  • Props: titleOriginal, headlineOriginal, synopsisOriginal, tags, coverImagePreview, metadataProposals, isGenerating, isUpdateMode
  • 機能:
    • ドラッグ&ドロップによる表紙画像アップロード
    • AI メタデータ生成(OpenRouter, Anthropic, Gemini, Zai, Ollama, LM Studio対応)
    • 生成結果の提案カードからワンクリック適用

LocalizationTab

  • Props: localizations, originalLanguage, plotTranslations, structuredPlot, manuscriptLanguages, isTranslating, translationInstructions
  • 機能:
    • 言語ステータス可視化(Complete / Partial / Recommended / Available)
    • AI翻訳(カスタム指示対応)
    • プロット構造翻訳(Part/Arc/Chapter/Storyのタイトル・目的の翻訳)
    • デスクトップ(マスター・ディテール)とモバイル(コンパクト)のレイアウト切替
    • 言語検索機能
    • 英語以外の原語の場合、英語翻訳が必須

QualityTab

  • Props: report, reportOriginal, isLoading, error, geminiApiKey
  • 機能:
    • デュアルモードレビュー(Global English / Original Language)
    • 公開可否判定と詳細レーティング
    • Gemini APIキー設定
    • 再生成機能

FinalTab

  • Props: isAuthenticated, title, headline, synopsis, tags, coverImagePreview, publishId, isNewPublishSlot, destinationSlots
  • 機能:
    • 英語/原語のサイドバイサイドプレビュー
    • 公開先選択(新規スロット / 既存スロット)
    • 認証状態の確認
    • 公開済みメタデータの取得・表示

3. 状態管理

3.1. Jotai Atoms

Publishウィザードの状態は publishStateAtoms.ts で一元管理されています。

export type TabId = "metadata" | "localization" | "quality" | "final";
export type TabStatus = "complete" | "incomplete";

export interface PublishState {
  projectId: string | null;
  publishId: string | null;
  isNewPublishSlot: boolean;
  activeTab: TabId;
  reviewReport: ReviewReport | null;
  reviewReportOriginal: ReviewReport | null;
  originalLanguage: string;
  localizations: Record<string, ProjectLocalization>;
  coverImagePreview: string | null;
  metadataProposals: GeneratedMetadata[];
  translationInstructions: string;
}
  • publishStateAtom: atomWithStorage で localStorage に永続化。キーは publish:wizardState
  • resetPublishStateAtom: 状態を initialPublishState にリセット
  • myPublishedSlotsAtom: ユーザーの既存公開スロット一覧
  • fetchMyPublishedSlotsAtom: /api/projects/my-published からスロットを取得

3.2. タブステータス判定

getTabStatus(tab, state) は各タブの完了状態を判定します:

  • metadata: 原語の localizations[originalLanguage]titlesynopsis が非空
  • localization: 原語が en なら自動完了。それ以外は localizations["en"]title/synopsis が非空
  • quality: reviewReport.overall_assessment.is_publishable が truthy
  • final: 上記3タスクがすべて complete

getAllTabStatuses(state) は全タブのステータスを一括取得します。

3.3. マイグレーション

migratePublishState() は旧フォーマット(titleOriginal, synopsisOriginal 等の個別フィールド)から新フォーマット(localizations マップ)への移行を custom storage の getItem 時に自動実行します。

旧フォーマットの検出条件: parsed.localizations がオブジェクトでない場合。

4. データモデル

4.1. ProjectLocalization

export interface ProjectLocalization {
  title: string;
  headline: string;
  synopsis: string;
  tags: string[];
}

各言語のメタデータを保持する単位。localizations: Record<string, ProjectLocalization> の形で、BCP 47言語タグをキーとするマップとして管理されます。

4.2. PublishRequestData

export interface PublishRequestData {
  id: string;
  title: string;
  headline: string;
  synopsis: string;
  original_language?: string;
  localizations?: Record<string, ProjectLocalization>;
  content: ProjectContent;
  review_report?: Json;
  cover_image_url?: string | null;
}

Publish APIに送信されるデータ構造。localizations JSONBが唯一の多言語メタデータソース。title/headline/synopsisはトップレベルのフォールバック値(英語)として保持。

4.3. ProjectContent

export interface ProjectContent {
  structuredPlot: StructuredPlot;
  manuscripts: Manuscript;
  writingPrompts?: PromptCard[];
  plotTranslations?: {
    [lang: string]: {
      [id: string]: PlotTranslation;
    };
  };
}

4.4. PlotTranslation

export interface PlotTranslation {
  title?: string;    // Part/Arc/Chapter/Story のタイトル
  purpose?: string;  // 目的・テーマ
  content?: string;  // セクションの要約
}

プロット構造の多言語翻訳データ。要素IDをキーとし、各言語ごとに管理されます。

4.5. PublishedSlot

export interface PublishedSlot {
  id: string;
  title: string;
  headline: string | null;
  synopsis: string | null;
  tags: string[] | null;
  cover_image_url: string | null;
  published_at: string | null;
  updated_at: string | null;
}

ユーザーの既存公開スロット情報。FinalTabの公開先選択で使用されます。

4.6. published_projects テーブル(Supabase)

主要カラム:

カラム説明
idUUID (PK)プロジェクトID
user_idUUID (FK → auth.users)所有者
titleTEXTグローバル(英語)タイトル
headlineTEXTグローバル見出し
synopsisTEXTグローバルあらすじ
tagsTEXT[]タグ配列(DB未DROP: コードからは完全に除去済み。localizations[lang].tags を使用)
title_originalTEXT原語タイトル(DB未DROP: コードからは完全に除去済み。localizations[original_language].title を使用)
headline_originalTEXT原語見出し(DB未DROP: コードからは完全に除去済み。localizations[original_language].headline を使用)
synopsis_originalTEXT原語あらすじ(DB未DROP: コードからは完全に除去済み。localizations[original_language].synopsis を使用)
original_languageTEXT原語のBCP 47言語タグ
localizationsJSONB言語別メタデータマップ(唯一のメタデータソース)
default_languageTEXTデフォルト表示言語
display_titleTEXT検索・表示用タイトル
cover_image_urlTEXT表紙画像URL
content_pathTEXTStorage内のコンテンツパス
review_reportJSONBAI品質レビュー結果
published_atTIMESTAMPTZ公開日時
updated_atTIMESTAMPTZ更新日時

インデックス:

  • idx_published_projects_display_title: GIN trigram(テキスト検索用)
  • idx_published_projects_localizations: GIN(JSONBクエリ用)

RLS ポリシー:

  • SELECT: 全ユーザー公開(true
  • INSERT/UPDATE/DELETE: 所有者のみ(auth.uid() = user_id

5. Localization Studio

5.1. 言語ステータス判定

getLanguageStatus() により、各言語のローカライズ完了度が4段階で判定されます:

ステータス条件アイコン
Completetitle と synopsis が非空CheckCircle2 (text-success)
Partialtitle または synopsis のいずれかが非空AlertCircle (text-warning)
Recommendedマニュスクリプトにその言語の原稿が存在するLightbulb (text-primary)
Available上記いずれにも該当しないなし(デフォルト表示)

原語の場合は Recommended 判定がスキップされ、Complete / Partial / Available の3段階となります。

5.2. supported-languages.json

public/languages/supported-languages.json に対応言語一覧が定義されています:

[
  {
    "code": "en",
    "name": "English",
    "nativeName": "English",
    "uiSupported": true
  },
  {
    "code": "ja",
    "name": "Japanese",
    "nativeName": "日本語",
    "uiSupported": true
  },
  {
    "code": "zh-Hans",
    "name": "Chinese (Simplified)",
    "nativeName": "简体中文",
    "uiSupported": true
  },
  {
    "code": "ko",
    "name": "Korean",
    "nativeName": "한국어",
    "uiSupported": false
  }
]

uiSupported: true の言語はUI表示に最適化されています。

5.3. AI翻訳の統合

  • AI翻訳は各AIプロバイダー(Gemini, OpenRouter等)を経由して実行
  • translationInstructions によりユーザーがカスタム指示を追加可能
  • メタデータ翻訳とプロット構造翻訳の2系統が存在
  • プロット翻訳は updatePlotTranslation(lang, id, field, value) で要素単位に更新

5.4. レイアウト

  • デスクトップ: マスター・ディテール形式(言語一覧 + 詳細編集パネル)
  • モバイル: コンパクト表示(アコーディオン展開)

6. Publish実行フロー

6.1. 処理シーケンス

publishProject() 関数(lib/features/publish/service.ts)が実行する一連の処理:

1. 表紙画像アップロード
   ├─ file が存在する場合
   │  ├─ covers バケットに {userId}/{projectId}.{ext} でアップロード
   │  └─ 公開URL取得
   └─ 既存 cover_image_url を使用

2. マニュスクリプトの順序整理
   ├─ reorderManuscriptsByPlot() で構造順に並び替え
   └─ Parts → Arcs → Chapters → Stories → Sections の順でトラバース

3. コンテンツJSONのStorage保存
   ├─ {userId}/{projectId}.json として contents バケットにアップロード
   ├─ upsert: true(既存なら上書き)
   └─ contentType: application/json

4. DB保存(upsert)
   ├─ 既存スロットの所有権確認
   ├─ localizations マップをそのまま使用(クライアント側で構築済み)
   ├─ display_title の決定: defaultLang → GLOBAL_LANGUAGE → title の順
   └─ published_projects テーブルに upsert

6.2. コンテンツ検証

validateProjectContent()lib/features/publish/validate.ts)による検証:

検査項目レベル内容
Zodスキーマ検証ErrorProjectContentの構造的妥当性
プロット構造空チェックErrorparts/chaptersのいずれも存在しない
孤立原稿Warningマニュスクリプトに存在するがプロットにないセクションID
未執筆セクションWarningプロットに存在するがマニュスクリプトがないセクション
空原稿Warning全バージョンが空のマニュスクリプト

7. コンポーネント一覧

Publish画面構成コンポーネント

コンポーネント場所責務
PublishPageapp/project/[id]/publish/page.tsxServer Component。認証チェック、データ取得
PublishWizardcomponents/publish/クライアント側ウィザード管理(タブ切替、フッター)
MetadataTabcomponents/publish/tabs/MetadataTab.tsxメタデータ入力、AI生成、表紙画像
LocalizationTabcomponents/publish/tabs/LocalizationTab.tsx多言語管理、AI翻訳、言語ステータス表示
QualityTabcomponents/publish/tabs/QualityTab.tsxAI品質レビュー、結果表示
FinalTabcomponents/publish/tabs/FinalTab.tsx最終プレビュー、公開先選択、公開実行
AiSettingsSheetcomponents/publish/AIプロバイダー設定シート
MetadataEditModalcomponents/MetadataEditModal.tsx廃止済み(Publishウィザードが代替)

MetadataEditModal(廃止済み)

公開済み作品のメタデータを編集するモーダル。Publishウィザードが上位互換として機能するため、2026-05-18に廃止。 メタデータ編集はPublishウィザード(/project/[id]/publish)経由で行う。

Unpublish

非公開化フロー:

  • API: DELETE /api/projects/:id/unpublish
  • Service: unpublishProject(supabase, userId, projectId)
  • 確認ダイアログ付き

8. データフロー図

┌─────────────────────────────────────────────────────────────┐
│                    Publish Wizard UI                         │
│  ┌──────────┐ ┌──────────────┐ ┌─────────┐ ┌──────────┐   │
│  │ Metadata │ │ Localization │ │ Quality │ │  Final   │   │
│  │   Tab    │→│     Tab      │→│   Tab   │→│   Tab    │   │
│  └────┬─────┘ └──────┬───────┘ └────┬────┘ └────┬─────┘   │
│       │               │              │            │         │
└───────┼───────────────┼──────────────┼────────────┼─────────┘
        │               │              │            │
        ▼               ▼              ▼            ▼
┌─────────────────────────────────────────────────────────────┐
│                   publishStateAtom (Jotai)                   │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ PublishState {                                        │  │
│  │   localizations: Record<string, ProjectLocalization>  │  │
│  │   reviewReport: ReviewReport | null                   │  │
│  │   coverImagePreview: string | null                    │  │
│  │   ...                                                 │  │
│  │ }                                                     │  │
│  └───────────────────────┬───────────────────────────────┘  │
└──────────────────────────┼──────────────────────────────────┘
                           │ handlePublish()

┌─────────────────────────────────────────────────────────────┐
│              POST /api/publish                               │
│  1. 表紙画像 → covers バケット (Storage)                     │
│  2. コンテンツ → contents バケット (Storage)                  │
│  3. メタデータ → published_projects (Database)                │
└─────────────────────────────────────────────────────────────┘

9. API エンドポイント

メソッドパス説明
POST/api/publish作品公開(メインエンドポイント)
GET/api/projects/my-published自分の公開済み作品一覧
POST/api/projects/status公開ステータス確認
GET/api/projects/:id/metadata公開済みメタデータ取得
PATCH/api/projects/:id/metadataメタデータ更新
GET/api/projects/:id/contentコンテンツ取得
DELETE/api/projects/:id/unpublish非公開化
POST/api/ai/reviewAI品質レビュー
POST/api/ai/metadata-generateAIメタデータ生成
POST/api/ai/metadata-translateAI翻訳

10. Hooks

usePublishActions

hooks/usePublishActions.ts にPublish操作を集約したカスタムフック:

  • handleStartReview() - 品質レビュー開始
  • handleGenerateMetadata() - AI メタデータ生成
  • executeTranslation(targetLang) - 指定言語への翻訳実行
  • handlePublish() - 公開実行
  • selectDestination(id, isNew) - 公開先スロット選択
  • saveMetadata() - ローカル状態を IndexedDB に保存

11. 関連ドキュメント

On this page