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_LANGUAGE) | lang=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_lang → default_language → GLOBAL_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_projectsRPC でlocalizations内全言語を横断検索 - タグフィルタ:
get_projects_by_tagRPC で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;
}レスポンシブグリッド:
| ブレークポイント | カラム数 |
|---|---|
| < 640px | 2 |
| 640px - 768px | 3 |
| 768px - 1024px | 3 |
| 1024px - 1280px | 4 |
| 1280px - 1536px | 6 |
| > 1536px | 7 |
アニメーション:
- 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_LANGUAGE は lib/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関連
| コンポーネント | 場所 | 責務 |
|---|---|---|
| ExplorePage | app/explore/page.tsx | Server Component。データ取得 |
| ExplorePageClient | app/explore/ExplorePageClient.tsx | Client Component。UI状態管理 |
| ExpandableGrid | components/ExpandableGrid.tsx | 汎用展開グリッド |
| ExploreSearchControls | components/ExploreSearchControls.tsx | 検索バー + タグフィルタ |
| ProjectCard | components/ProjectCard.tsx | 作品カード |
| ProjectDetailView | components/ProjectDetailView.tsx | 作品詳細表示(インライン展開) |
| OriginalLanguageBadge | components/ | 原稿言語バッジ |
| PaginationControls | components/ | ページネーション |
Library関連
| コンポーネント | 場所 | 責務 |
|---|---|---|
| LibraryPage | app/explore/library/page.tsx | Server Component |
| LibraryPageClient | app/explore/library/LibraryPageClient.tsx | Client Component。DnD並び替え |
My Works関連
| コンポーネント | 場所 | 責務 |
|---|---|---|
| MyWorksPage | app/my-works/ | 自分の作品管理 |
サービス
| モジュール | 場所 | 責務 |
|---|---|---|
| projects/service | lib/features/projects/service.ts | getPublishedProjects, getPublishedProjectsCount |
| tags/service | lib/features/tags/service.ts | getTagsByContext, getPopularTags |
| library/service | lib/features/library/service.ts | ライブラリCRUD操作 |
| metadataUtils | lib/i18n/metadataUtils.ts | getDisplayMetadata, getDisplayTags, getLocalizationLanguages, parseLocalization, getLocalizationsMap |
| constants | lib/i18n/constants.ts | GLOBAL_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への移行
localizationsJSONB がタグ・検索の単一ソースとして機能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. 関連ドキュメント
- 22_publish_system - Publishシステム(公開フローの上流)
- 24_reader_system - Readerシステム(閲覧の下流)
- 03_state_management - Jotai状態管理の全体設計
- 04_database_schema_supabase - Supabaseデータベーススキーマ
- 07_locale_system - BCP 47言語タグの管理方式
- 07_i18n - 国際化システム
- 09_design_system - デザインシステム(Beyond Numbers哲学)
- 05_explore_components - Explore関連コンポーネント詳細
- 06_details_page_components - 詳細ページコンポーネント
- 04_library_components - ライブラリコンポーネント