😊

【開発ログ】学習カード登録にタグ機能を追加

に公開

画像アップロードの動作を確認しているうちに、タグ分けができていないことに気付きました。
そこで、タグだけ管理できるようにテーブルを分け、Word+画像+タグを同時に登録できるように実装しました。


前回

  • 画像アップロードの動作確認
  • 単語を DB / Firebase へ保存する処理の検証
  • エラー発生時のログ確認とハンドリング方法の記録

今回

  • タグ機能(検索・フィルタリング対応も検討)
  • Word 登録時に既存タグを選択 or 新規タグを作成できるよう修正
  • タグ一覧をフロントから取得して UI で選択可能に

① Prisma スキーマ修正

  • Tag モデルを追加
  • WordTag を N:N 関係にする
model Tag {
  id        String   @id @default(cuid())
  name      String   @unique
  createdAt DateTime @default(now())
  words     Word[]   @relation("WordTags")
}

model Word {
  id        String   @id @default(cuid())
  jaSurface String
  koSurface String
  createdAt DateTime @default(now())
  imageId   String?

  tags Tag[] @relation("WordTags")
}

② API 修正

単語登録の際に タグを新規登録または既存タグに接続できるように connectOrCreate を利用。

リクエスト例

{
  "jaSurface": "犬",
  "koSurface": "개",
  "tags": ["動物", "ペット"],
  "imageUrl": "...",
  "storagePath": "...",
  "contentType": "image/png"
}

処理抜粋

tags: {
  connectOrCreate: (tags ?? []).map((t: string) => ({
    where: { name: t },
    create: { name: t },
  })),
}
  • 既存タグ → connect(再利用)
  • 新規タグ → create(新規作成)

更新時は一度 set: [] で既存タグをリセットしてから再設定。


③ コンポーネント修正

  • タグ一覧を API から取得
  • タグボタンのトグル選択 + 新規タグ入力 (Enter)
  • 選択済みタグは強調表示
  • 保存後はフォームと選択状態をリセット
// タグ一覧を取得
useEffect(() => {
  const fetchTags = async () => {
    const res = await fetch("/api/tags");
    const data = await res.json();
    setAllTags(data);
  };
  fetchTags();
}, []);

// タグ選択トグル
const toggleTag = (tagName: string) => {
  setForm((prev) => {
    const already = prev.tags.includes(tagName);
    return {
      ...prev,
      tags: already
        ? prev.tags.filter((t) => t !== tagName)
        : [...prev.tags, tagName],
    };
  });
};

// 新しいタグを追加
const addNewTag = (e: React.KeyboardEvent<HTMLInputElement>) => {
  if (e.key === "Enter") {
    e.preventDefault();
    const value = e.currentTarget.value.trim();
    if (value && !form.tags.includes(value)) {
      setForm((prev) => ({ ...prev, tags: [...prev.tags, value] }));
    }
    e.currentTarget.value = "";
  }
};

UI部品(抜粋)

{/* タグ選択 */}
<div className="flex flex-wrap gap-2 mb-3">
  {allTags.map((tag) => (
    <button
      key={tag.id}
      type="button"
      onClick={() => toggleTag(tag.name)}
      className={`px-3 py-1 rounded-full border ${
        form.tags.includes(tag.name)
          ? "bg-gray-100"
          : "bg-black text-white"
      }`}
    >
      {tag.name}
    </button>
  ))}
</div>

{/* 新規タグ入力 */}
<input
  type="text"
  placeholder="新しいタグを入力して Enter"
  onKeyDown={addNewTag}
  className="w-full border rounded px-3 py-1"
/>

結果

タグ選択UIのスクリーンショット

タグ選択


悩んだこと / 学んだこと

  • Prisma の N:N 関係では connectOrCreate が便利
    → 既存なら接続、なければ新規作成して接続できる
  • UI/UX の観点からは、毎回タグ入力より「既存タグ選択 + 新規追加」が自然
  • 保存後のタグリストを再フェッチする処理を入れて、即座に反映できるようにした

次回やること

  • タグ別検索・フィルタリングの実装
  • レッスンカードを API から取得
  • UI デザインの再調整(ボタンの色・一覧性改善)

Discussion