コンテンツにスキップ

お気に入りプラグイン (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;
}

セキュリティ考慮事項

  1. 認証必須: すべての操作で認証をチェック
  2. 所有者制限: 顧客は自分のお気に入りのみ操作可能
  3. 商品存在確認: 存在しない商品IDの登録を防止
  4. 最大数制限: DoS攻撃を防ぐため登録数を制限

パフォーマンス最適化

  • キャッシング: お気に入り商品一覧をRedisにキャッシュ(5分TTL)
  • 遅延ロード: 商品詳細は必要に応じて取得
  • バッチ取得: 複数商品をまとめて取得(N+1問題を回避)

トラブルシューティング

よくある問題

問題: お気に入りが保存されない

  • 原因: 認証エラー
  • 解決: ログイン状態を確認

問題: お気に入り一覧が空

  • 原因: 商品が削除された、または非公開になった
  • 解決: 商品の存在と公開状態を確認

問題: 最大数に達してエラー

  • 原因: 100商品以上登録しようとした
  • 解決: 古いお気に入りを削除

関連ドキュメント

今後の拡張予定

  • お気に入り商品の並び替え機能
  • お気に入りリストの共有機能
  • お気に入り商品の在庫通知
  • お気に入り商品の価格変動通知
  • お気に入り商品の統計・分析機能

制限事項

現在の制限

  1. タイムスタンプなし: お気に入り登録日時は記録されない
  2. 分析機能制限: どの商品が人気かの統計は取れない
  3. 最大登録数: デフォルトで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; // 閲覧回数
}