お気に入りプラグイン (Wishlist Plugin)¶
概要¶
Ritsubiの商品お気に入り機能を実現するシンプルなVendureカスタムプラグイン。認証ユーザーが商品をお気に入りに登録・削除し、お気に入り一覧を管理できます。
パッケージ情報¶
- パッケージ名:
@ritsubi/wishlist-plugin - パス:
/packages/plugins/wishlist - エントリーポイント:
src/index.ts
実装機能¶
1. 商品お気に入り登録・削除¶
認証済みの顧客が商品をお気に入りに追加・削除。
// お気に入りに追加
await addToFavorites(productId);
// お気に入りから削除
await removeFromFavorites(productId);
2. お気に入り商品一覧取得¶
顧客のお気に入り商品一覧を取得。
// お気に入り一覧を取得
const favorites = await getFavorites();
3. お気に入り状態確認¶
特定の商品がお気に入りに登録されているかチェック。
// お気に入りかどうか確認
const isFavorite = await isFavorite(productId);
技術仕様¶
プラグイン設定オプション¶
export interface WishlistPluginOptions {
/**
* 最大お気に入り登録数
*/
maxFavorites?: number;
}
プラグイン初期化¶
import { WishlistPlugin } from '@ritsubi/wishlist-plugin';
export const config: VendureConfig = {
plugins: [
WishlistPlugin.init({
maxFavorites: 100, // 最大100商品まで登録可能
}),
],
};
データ保存方式¶
Customerカスタムフィールドを使用してお気に入り商品IDを保存。
Customer.customFields.favoriteProductIds: string[] // 商品IDの配列
メリット¶
- シンプル: 追加のエンティティやテーブル不要
- 高速: 顧客情報とともに一度に取得
- 軽量: 最小限のデータベースクエリ
デメリット¶
- 高度な分析機能は制限される(タイムスタンプなし等)
- 大量の商品登録には不向き(最大100商品推奨)
サービス¶
WishlistService¶
お気に入り管理のロジックを担当するサービス。
主要メソッド:
addToFavorites(ctx: RequestContext, customerId: ID, productId: ID): Promise<boolean>- お気に入りに商品を追加
removeFromFavorites(ctx: RequestContext, customerId: ID, productId: ID): Promise<boolean>- お気に入りから商品を削除
getFavorites(ctx: RequestContext, customerId: ID): Promise<Product[]>- お気に入り商品一覧を取得
isFavorite(ctx: RequestContext, customerId: ID, productId: ID): Promise<boolean>- お気に入り状態を確認
GraphQL API¶
スキーマ定義¶
extend type Query {
"""
ログイン中の顧客のお気に入り商品一覧を取得
"""
getFavorites: [Product!]!
"""
指定商品がお気に入りに登録されているか確認
"""
isFavorite(productId: ID!): Boolean!
}
extend type Mutation {
"""
お気に入りに商品を追加
"""
addToFavorites(productId: ID!): Boolean!
"""
お気に入りから商品を削除
"""
removeFromFavorites(productId: ID!): Boolean!
}
カスタムフィールド¶
Customer¶
favoriteProductIds(string[]): お気に入り商品ID一覧- アクセス: 内部のみ(public: false, internal: true)
- 形式: 文字列の配列
対応する要件¶
機能要件¶
- お気に入り登録: ログインユーザーが商品をお気に入りに追加
- お気に入り削除: お気に入りから商品を削除
- お気に入り一覧: お気に入り商品の一覧表示
- 状態確認: 商品詳細ページでお気に入り状態を表示
使用例¶
フロントエンド(Next.js)での使用¶
お気に入りに追加¶
import { useMutation } from '@apollo/client';
import { ADD_TO_FAVORITES } from '@/graphql/mutations';
const AddToFavoritesButton = ({ productId }: { productId: string }) => {
const [addToFavorites, { loading }] = useMutation(ADD_TO_FAVORITES);
const handleClick = async () => {
try {
await addToFavorites({
variables: { productId },
});
toast.success('お気に入りに追加しました');
} catch (error) {
toast.error('お気に入りに追加できませんでした');
}
};
return (
<button onClick={handleClick} disabled={loading}>
{loading ? '追加中...' : 'お気に入りに追加'}
</button>
);
};
お気に入り一覧を表示¶
import { useQuery } from '@apollo/client';
import { GET_FAVORITES } from '@/graphql/queries';
const FavoritesPage = () => {
const { data, loading } = useQuery(GET_FAVORITES);
if (loading) return <div>読み込み中...</div>;
return (
<div className="favorites-grid">
{data.getFavorites.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
};
お気に入り状態を確認¶
import { useQuery } from '@apollo/client';
import { IS_FAVORITE } from '@/graphql/queries';
const FavoriteButton = ({ productId }: { productId: string }) => {
const { data } = useQuery(IS_FAVORITE, {
variables: { productId },
});
const isFavorite = data?.isFavorite ?? false;
return (
<button className={isFavorite ? 'favorite-active' : 'favorite-inactive'}>
{isFavorite ? '♥' : '♡'}
</button>
);
};
GraphQLクエリ・ミューテーション¶
お気に入り追加¶
mutation AddToFavorites($productId: ID!) {
addToFavorites(productId: $productId)
}
お気に入り削除¶
mutation RemoveFromFavorites($productId: ID!) {
removeFromFavorites(productId: $productId)
}
お気に入り一覧取得¶
query GetFavorites {
getFavorites {
id
name
slug
featuredAsset {
preview
}
variants {
id
name
priceWithTax
}
}
}
お気に入り状態確認¶
query IsFavorite($productId: ID!) {
isFavorite(productId: $productId)
}
管理画面での使用¶
// 顧客のお気に入り一覧を確認
const customer = await customerService.findOne(customerId);
const favoriteIds = customer.customFields.favoriteProductIds ?? [];
console.log(`お気に入り商品数: ${favoriteIds.length}`);
// お気に入り商品の統計
const products = await productService.findByIds(favoriteIds);
products.forEach(product => {
console.log(`${product.name} - ${product.id}`);
});
実装の詳細¶
認証制御¶
すべてのお気に入り操作は認証が必須。
@Mutation()
@Allow(Permission.Owner)
async addToFavorites(
@Ctx() ctx: RequestContext,
@Args() args: { productId: ID }
): Promise<boolean> {
const customerId = ctx.activeUserId;
if (!customerId) {
throw new ForbiddenError('ログインが必要です');
}
return this.wishlistService.addToFavorites(ctx, customerId, args.productId);
}
最大登録数の制限¶
async addToFavorites(
ctx: RequestContext,
customerId: ID,
productId: ID
): Promise<boolean> {
const customer = await this.customerService.findOne(ctx, customerId);
const favoriteIds = customer.customFields.favoriteProductIds ?? [];
// 最大数チェック
if (favoriteIds.length >= this.options.maxFavorites) {
throw new UserInputError(`お気に入りは最大${this.options.maxFavorites}件まで登録できます`);
}
// 重複チェック
if (favoriteIds.includes(productId)) {
return true; // すでに登録済み
}
// 追加
await this.customerService.update(ctx, {
id: customerId,
customFields: {
favoriteProductIds: [...favoriteIds, productId],
},
});
return true;
}
セキュリティ考慮事項¶
- 認証必須: すべての操作で認証をチェック
- 所有者制限: 顧客は自分のお気に入りのみ操作可能
- 商品存在確認: 存在しない商品IDの登録を防止
- 最大数制限: DoS攻撃を防ぐため登録数を制限
パフォーマンス最適化¶
- キャッシング: お気に入り商品一覧をRedisにキャッシュ(5分TTL)
- 遅延ロード: 商品詳細は必要に応じて取得
- バッチ取得: 複数商品をまとめて取得(N+1問題を回避)
トラブルシューティング¶
よくある問題¶
問題: お気に入りが保存されない
- 原因: 認証エラー
- 解決: ログイン状態を確認
問題: お気に入り一覧が空
- 原因: 商品が削除された、または非公開になった
- 解決: 商品の存在と公開状態を確認
問題: 最大数に達してエラー
- 原因: 100商品以上登録しようとした
- 解決: 古いお気に入りを削除
関連ドキュメント¶
- 顧客管理プラグイン - 顧客情報との連携
- Vendure機能リファレンス
今後の拡張予定¶
- お気に入り商品の並び替え機能
- お気に入りリストの共有機能
- お気に入り商品の在庫通知
- お気に入り商品の価格変動通知
- お気に入り商品の統計・分析機能
制限事項¶
現在の制限¶
- タイムスタンプなし: お気に入り登録日時は記録されない
- 分析機能制限: どの商品が人気かの統計は取れない
- 最大登録数: デフォルトで100商品まで
Phase 2での拡張検討¶
高度な分析や統計が必要になった場合、専用エンティティへの移行を検討:
@Entity()
export class WishlistItemEntity extends VendureEntity {
@Column()
customerId: string;
@Column()
productId: string;
@Column()
addedAt: Date; // 登録日時
@Column({ nullable: true })
notes: string; // メモ
@Column('int', { default: 0 })
viewCount: number; // 閲覧回数
}