BluePeriod Docs
開発

Explore System

BluePeriodのExplore(探索・閲覧)システムのアーキテクチャ、検索・フィルタ機能、多言語表示、ライブラリ管理を網羅的に解説

Explore System (23_explore_system.md)

1. 概要

Exploreシステムは、BluePeriodプラットフォーム上で公開された作品を閲覧・発見するための機能群です。作品の検索・フィルタリング、カード表示とインライン展開、多言語メタデータ表示、ライブラリ(ブックマーク)管理、自分の作品管理を統合的に提供します。

主な画面:

  • Explore: 全公開作品の閲覧・検索(/explore
  • Library: ユーザーのブックマーク管理(/explore/library
  • My Works: 自分の公開済み作品管理(/my-works

設計思想として「数字からの脱却(Beyond Numbers)」を掲げ、いいね数や閲覧数といった定量的指標を公共の場では表示せず、定性的な文脈を重視しています(09_design_system 参照)。

2. 画面構成

2.1. Explore(/explore

Server Component (page.tsx)

export const dynamic = "force-dynamic"; // 常に最新データを取得

// 並列データ取得
const [projects, totalItems, popularTags] = await Promise.all([
  getPublishedProjects(supabase, { page, query, tag }),
  getPublishedProjectsCount(supabase, { query, tag }),
  getTagsByContext(supabase, undefined, "global"),
]);
  • ページネーション: 12件/ページ
  • URLパラメータ: q(検索クエリ), tag(タグフィルタ), page(ページ番号)

Client Component (ExplorePageClient.tsx)

  • ExpandableGrid によるカード展開UI
  • ProjectDetailView オーバーレイ
  • ブックマークボタン
  • MetadataEditModal廃止済み: メタデータ編集はPublishウィザード経由)
  • Unpublish確認ダイアログ
  • Lightbox(表紙画像拡大表示)

2.2. Library(/explore/library

  • ユーザーのブックマーク一覧
  • ドラッグ&ドロップによる並び替え(@dnd-kit)
  • タイトル/あらすじ検索 + タグフィルタ
  • コンテキスト別タグ: getTagsByContext(supabase, userId, "library")
  • 削除時はAlertDialogによる確認

2.3. My Works(/my-works

  • 自分の公開済み作品一覧
  • ExplorePageClient を再利用(UI一貫性)
  • タグコンテキスト: getTagsByContext(supabase, userId, "my_works")
  • 専用アクション: メタデータ編集、非公開化
  • 空状態時のCTA(ブックシェルフへの誘導)

3. 検索・フィルタ機能

3.1. ExploreSearchControls

interface ExploreSearchControlsProps {
  popularTags: { tag: string; count: number }[];
  pageType?: "explore" | "library" | "my-works";
}

機能:

  • デバウンス検索(500ms)
  • タグフィルタ(Popover + Command形式の選択UI)
  • 横スクロール可能なタグバー
  • フィルタクリアボタン
  • ページタイプ別のヘッダーアイコン/タイトル

URLパラメータ管理:

パラメータ用途
q検索クエリ(title, synopsis, author)q=fantasy
tagアクティブなタグフィルタtag=sf
langタグ表示言語(デフォルト: GLOBAL_LANGUAGElang=ja
pageページ番号(デフォルト: 1)page=2

パラメータ変更時は router.push() でURLを更新し、Server Componentが再取得します。

3.2. Supabase RPC関数

get_localized_tags(p_lang, p_user_id, p_context)

CREATE OR REPLACE FUNCTION get_localized_tags(
  p_lang text,
  p_user_id uuid DEFAULT NULL,
  p_context text DEFAULT 'global'
)
RETURNS TABLE (tag text, count bigint)

localizations JSONB から指定言語のタグを抽出・集計。フォールバックチェーン: p_langdefault_languageGLOBAL_LANGUAGE("en")

p_context取得対象
global全公開作品
libraryユーザーのライブラリ内作品
my_worksユーザーの公開済み作品

TypeScript側: getLocalizedTags(supabase, userId, context, lang)

get_available_tag_languages(p_user_id, p_context)

CREATE OR REPLACE FUNCTION get_available_tag_languages(
  p_user_id uuid DEFAULT NULL,
  p_context text DEFAULT 'global'
)
RETURNS TABLE (lang text, tag_count bigint)

タグが1件以上存在する言語コード一覧を返す。言語セレクタUIの選択肢として使用。

get_projects_by_tag(p_tag, p_limit, p_offset)

CREATE OR REPLACE FUNCTION get_projects_by_tag(
  p_tag text,
  p_limit int DEFAULT 60,
  p_offset int DEFAULT 0
)
RETURNS TABLE (id uuid)

localizations JSONB 内の全言語エントリから指定タグを含むプロジェクトIDを検索。

search_published_projects(p_query, p_limit, p_offset)

CREATE OR REPLACE FUNCTION search_published_projects(
  p_query text,
  p_limit int DEFAULT 60,
  p_offset int DEFAULT 0
)
RETURNS TABLE (id uuid)

localizations JSONB 内の全言語の title / synopsis を横断検索(ILIKE)。

旧RPC(非推奨)

  • get_popular_tags() — 旧 tags カラムの unnest() に依存
  • get_tags_by_context(p_user_id, p_context) — 同上

これらは get_localized_tags に置き換えられました。

3.3. プロジェクト検索

export async function getPublishedProjects(
  supabase: SupabaseClient<Database>,
  options: { page?: number; query?: string; tag?: string }
): Promise<PublishedProjectWithAgent[]>
  • テキスト検索: search_published_projects RPC で localizations 内全言語を横断検索
  • タグフィルタ: get_projects_by_tag RPC で localizations 内全言語のタグを検索
  • ページネーション: offset/limit ベース(12件/ページ)
  • 著者情報 JOIN: profiles テーブルから username, avatar_url を取得
export async function getPublishedProjectsCount(
  supabase: SupabaseClient<Database>,
  options: { query?: string; tag?: string }
): Promise<number>

検索条件に一致する総件数を返し、ページネーションUIの計算に使用されます。

4. 作品カードとインライン展開

4.1. ExpandableGrid

interface ExpandableGridProps<T> {
  items: T[];
  renderCard: (item: T, isSelected: boolean, onClick: () => void) => ReactNode;
  renderDetail: (item: T, onClose: () => void) => ReactNode;
  keyExtractor: (item: T) => string;
  className?: string;
}

レスポンシブグリッド:

ブレークポイントカラム数
< 640px2
640px - 768px3
768px - 1024px3
1024px - 1280px4
1280px - 1536px6
> 1536px7

アニメーション:

  • framer-motion によるスムーズな展開/折りたたみ
  • レイアウトアニメーション
  • 詳細表示位置への自動スクロール
  • アクティブインジケーターアニメーション

4.2. ProjectCard

interface ProjectCardProps {
  title: string;
  authorName?: string | null;
  authorId?: string | null;
  coverImageUrl?: string | null;
  updatedAt?: string | null;
  status?: ProjectStatus;
  variant: "private" | "public" | "library";
  isSelected?: boolean;
  originalLanguage?: string | null;
}

機能:

  • ブックカバー風デザイン
  • ステータスバッジ(private/public/library)
  • コンテキストメニュー
  • OriginalLanguageBadge
  • ホバーアニメーション

5. 作品詳細表示

5.1. ProjectDetailView

interface ProjectDetailViewProps {
  project: PublishedProjectWithAuthor;
  onClose: () => void;
  isMyWorksPage?: boolean;
  onEditMetadata?: (p: PublishedProjectWithAuthor) => void;
  onUnpublish?: (p: PublishedProjectWithAuthor) => void;
  bookmarkedIds?: Set<string>;
  currentLang: string;
}

5.2. 言語切替(manualMode)

🌐ボタン(DropdownMenu)で localizations 内の任意の言語を直接選択します:

モード動作
Auto"auto"ユーザーのブラウザ言語に基づく自動選択
言語コード"ja" / "en" / ...指定言語のメタデータを表示(ドロップダウンから任意の言語を直接選択)

ProjectDetailView(インラインエクスパンダー)と ProjectDetailsContainer(詳細ページ)の両方で同じドロップダウンパターンを使用します。

"original" / "global" モードおよび順次巡回方式は廃止され、ドロップダウンによる直接選択に統一されました。

5.3. フォールバックチェーン

getDisplayMetadata() / getDisplayTags()lib/i18n/metadataUtils.ts)による表示テキストの決定:

localizations マップが存在する場合:
  1. localizations[preferredLang]
  2. localizations[defaultLanguage]
  3. localizations[GLOBAL_LANGUAGE]("en")
  4. 旧カラム(title, headline, synopsis, tags)

GLOBAL_LANGUAGElib/i18n/constants.ts"en" として定義される共有定数。旧カラムへのフォールバックは、旧形式データからの移行完了後に削除予定。

5.4. OriginalLanguageBadge

作品カードおよび詳細表示に、原稿言語を示すバッジを表示します。BCP 47言語タグに基づきます。

5.5. タグ表示

getDisplayTags()lib/i18n/metadataUtils.ts)を通じて、🌐トグルの言語切替に追従してタグが切り替わります。フォールバックチェーンは getDisplayMetadata と同一です。

6. タグシステム

6.1. データソース

タグの単一ソースは localizations JSONB です。旧 tags TEXT[] カラムは非推奨(書き込み停止済み)。

  • get_localized_tags(p_lang, ...) RPC: 指定言語のタグを集計
  • get_available_tag_languages(...): タグが存在する言語コード一覧を返す
  • get_projects_by_tag(p_tag, ...): 全言語横断で単一タグ検索
  • get_projects_by_tags(p_tags, ...): 複数タグのAND条件検索

6.2. UI側

  • ExploreSearchControls 内のタグバー(横スクロール)
  • Popover + Command による複数タグ選択UI(AND条件)
  • タグクリックでトグル(追加/削除)、URLパラメータ ?tags=xxx,yyy で管理
  • 各ページタイプでコンテキストに応じたタグ一覧を表示

6.3. 言語セレクタ

ExploreSearchControls に言語選択ドロップダウン(Select コンポーネント)を配置。get_available_tag_languages で取得した言語一覧から選択でき、選択言語に応じてタグ一覧が切り替わる。URLパラメータ ?lang=xx で言語コンテキストを管理する。

7. ライブラリ管理

7.1. データモデル

CREATE TABLE library_items (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  project_id uuid NOT NULL REFERENCES published_projects(id) ON DELETE CASCADE,
  display_order INTEGER NOT NULL DEFAULT 0,
  added_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(user_id, project_id)
);

RLS ポリシー: ユーザー自身のアイテムのみアクセス可能(auth.uid() = user_id)。

7.2. API エンドポイント

メソッドパス説明
GET/api/libraryユーザーのライブラリ取得
POST/api/libraryライブラリに追加
DELETE/api/library?projectId={id}ライブラリから削除
PATCH/api/library表示順更新

7.3. UI機能

  • ブックマークボタン(楽観的更新)
  • ドラッグ&ドロップ並び替え(@dnd-kit)
  • 削除確認ダイアログ(AlertDialog)
  • トースト通知

8. コンポーネント一覧

Explore関連

コンポーネント場所責務
ExplorePageapp/explore/page.tsxServer Component。データ取得
ExplorePageClientapp/explore/ExplorePageClient.tsxClient Component。UI状態管理
ExpandableGridcomponents/ExpandableGrid.tsx汎用展開グリッド
ExploreSearchControlscomponents/ExploreSearchControls.tsx検索バー + タグフィルタ
ProjectCardcomponents/ProjectCard.tsx作品カード
ProjectDetailViewcomponents/ProjectDetailView.tsx作品詳細表示(インライン展開)
OriginalLanguageBadgecomponents/原稿言語バッジ
PaginationControlscomponents/ページネーション

Library関連

コンポーネント場所責務
LibraryPageapp/explore/library/page.tsxServer Component
LibraryPageClientapp/explore/library/LibraryPageClient.tsxClient Component。DnD並び替え

My Works関連

コンポーネント場所責務
MyWorksPageapp/my-works/自分の作品管理

サービス

モジュール場所責務
projects/servicelib/features/projects/service.tsgetPublishedProjects, getPublishedProjectsCount
tags/servicelib/features/tags/service.tsgetTagsByContext, getPopularTags
library/servicelib/features/library/service.tsライブラリCRUD操作
metadataUtilslib/i18n/metadataUtils.tsgetDisplayMetadata, getDisplayTags, getLocalizationLanguages, parseLocalization, getLocalizationsMap
constantslib/i18n/constants.tsGLOBAL_LANGUAGE ("en")

9. データフロー図

┌──────────────────────────────────────────────────────────────┐
│                    /explore (Server Component)                │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ Promise.all([                                          │  │
│  │   getPublishedProjects(supabase, {page, query, tag}),  │  │
│  │   getPublishedProjectsCount(supabase, {query, tag}),   │  │
│  │   getTagsByContext(supabase, undefined, "global")      │  │
│  │ ])                                                     │  │
│  └────────────────────┬───────────────────────────────────┘  │
└───────────────────────┼──────────────────────────────────────┘
                        │ props: projects, totalItems, popularTags

┌──────────────────────────────────────────────────────────────┐
│                ExplorePageClient (Client Component)           │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  ExploreSearchControls                                 │  │
│  │  ┌──────────┐  ┌──────────────────┐                    │  │
│  │  │ SearchBar│  │ TagFilter (Popover)│                    │  │
│  │  └──────────┘  └──────────────────┘                    │  │
│  └────────────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  ExpandableGrid                                        │  │
│  │  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐                  │  │
│  │  │ Card │ │ Card │ │ Card │ │ Card │  ← renderCard()   │  │
│  │  └──┬───┘ └──────┘ └──────┘ └──────┘                  │  │
│  │     ▼ click                                             │  │
│  │  ┌──────────────────────────────────────┐               │  │
│  │  │     ProjectDetailView               │  ← renderDetail│  │
│  │  │  🌐 Language Switch (auto/original/ │               │  │
│  │  │     global) → getDisplayMetadata()  │               │  │
│  │  └──────────────────────────────────────┘               │  │
│  └────────────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  PaginationControls                                    │  │
│  │  ←  1  2  3  ...  →                                    │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

10. 既知の課題・移行予定

localizations JSONBへの移行

  • localizations JSONB がタグ・検索の単一ソースとして機能
  • getDisplayMetadata() / getDisplayTags() は localizations ベースのフォールバックチェーンを実装
  • 詳細ページ(ProjectDetailsContainer)も getDisplayMetadata() に一本化済み(旧カラム直参照を排除)
  • tags TEXT[] カラムは書き込み停止済み(次回マイグレーションで削除予定)

タグの多言語対応

  • 完了: 言語セレクタによるタグ一覧の切り替え、🌐ドロップダウンによるタグ追従
  • 完了: 複数タグ選択(AND条件)対応。URLパラメータ ?tags=xxx,yyy
  • Libraryページのタグフィルタは getDisplayTags() 経由で localizations ベースに更新済み

言語バッジのi18n対応

  • 完了: LanguageLabel / OriginalLanguageBadge がUI言語設定に追従して言語名を表示
  • 翻訳キー(language.ja, language.en 等)は各言語の translation.json に定義

ページネーションの改善余地

  • 現在は従来型ページネーション(offset/limit)
  • 無限スクロール(Infinite Scroll)への移行が検討可能

11. 関連ドキュメント

On this page