👩‍💻

子ども向け学習アプリ開発ログ:学習APIの拡張 => 今日の単語・テスト単語・ダッシュボード連携

に公開

前回

  • タグを登録し、選択中のタグを外せるようにすること
  • UI デザインの再調整(ボタンの色・一覧性改善)

今回

  • 学習関連API
    • 今日の単語を10件, 学習済み単語からランダムに10件抽出(テスト用)
    • 取得データをダッシュボードに反映

① 今日の単語(10件)

「今日の単語」では、まだ学習していない単語の中から10件を取得します。

/api/words/today/route.ts
import { prisma } from "@/lib/prisma";
import { NextResponse } from "next/server";

export async function GET() {
  try {
    // ユーザ情報取得
    const decoded = "ユーザ情報取得";
    if (!user) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    // 学習完了ID取得(learn / quiz_end)
    const learned = await prisma.studyEvent.findMany({
      where: {
        OR: [{ action: "learn" }, { action: "quiz_end" }],
        userId: decoded.uid,
      },
      select: { wordId: true },
    });

    const learnedId = [...new Set(learned.map((e) => e.wordId))];

    // 今日の単語(未学習+古い順10件)
    const todayWords = await prisma.word.findMany({
      where: {
        userId: decoded.uid,
        status: "published",
        NOT: { id: { in: learnedId } }, // 学習完了は除外
      },
      orderBy: { createdAt: "asc" },
      take: 10,
      include: { image: true, tags: true },
    });

    if (todayWords.length === 0) {
      return NextResponse.json({ message: "No words to learn today" });
    }

    return NextResponse.json(todayWords);
  } catch (error) {
    console.error(error);
    return NextResponse.json({ error: "server error" }, { status: 500 });
  }
}

② テスト単語(10件)

「テスト単語」では、学習が完了した単語(learn または quiz_end)の中からランダムに10件を取得します。

/api/words/test/route.ts
import { prisma } from "@/lib/prisma";
import { NextResponse } from "next/server";

export async function GET() {
  try {
    // ユーザ情報取得
    const decoded = "ユーザ情報取得";
    if (!decoded) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    // 学習完了済み wordId 取得(learn / quiz_end)
    const learned = await prisma.studyEvent.findMany({
      where: {
        OR: [{ action: "learn" }, { action: "quiz_end" }],
        userId: decoded.uid,
      },
      select: { wordId: true },
    });

    const learnedId = [...new Set(learned.map((e) => e.wordId))];

    if (learnedId.length === 0) {
      return NextResponse.json({ message: "No learned words yet" });
    }

    // ランダム抽出(Prismaでは直接ランダムが使えないためJSでシャッフル)
    const shuffled = learnedId.sort(() => Math.random() - 0.5).slice(0, 10);

    // テスト単語取得
    const testWords = await prisma.word.findMany({
      where: {
        userId: decoded.uid,
        status: "published",
        id: { in: shuffled },
      },
      include: { image: true, tags: true },
    });

    if (testWords.length === 0) {
      return NextResponse.json({ message: "No words to test today" });
    }

    return NextResponse.json(testWords);
  } catch (error) {
    console.error(error);
    return NextResponse.json({ error: "server error" }, { status: 500 });
  }
}

③ Dashboardへの反映確認

以前作成した Dashboard に、学習イベントの累計が正しく反映されるかを確認しました。
テストとして、1つの単語に対して action: "learn" を記録し、今日の学習語数と累計語数 にカウントが加算されることを確認しています。
(※ 学習時間はまだ未設定のため、今回は「0」と表示されればOKです)

StudyEventaction 条件を追加

getDashboardData() 内の studyEvent クエリに action 条件を追加し、「学習完了(learn / quiz_end)」のみをカウント するように修正しました。

lib/dashboard.ts (抜粋)
const [
  todaySessions,
  monthSessions,
  allSessions,
  todayWordsDistinct,
  totalWordsDistinct,
] = await Promise.all([
  prisma.studyEvent.findMany({
    where: {
      userId,
      createdAt: { gte: start, lte: end },
      action: { in: ["learn", "quiz_end"] },
    },
    distinct: ["wordId"],
    select: { wordId: true },
  }),
  prisma.studyEvent.findMany({
    where: {
      userId,
      action: { in: ["learn", "quiz_end"] },
    },
    distinct: ["wordId"],
    select: { wordId: true },
  }),
]);

これにより、単語登録だけではカウントされず、実際に学習完了した単語だけが Dashboard に反映 されます。

ダッシュボード


悩んだこと / 学んだこと

  • Prismaでは orderBy: "random" が使えないため、JavaScript側で Array.sort(() => Math.random() - 0.5) を利用してランダム化。

  • Firebase UIDではなく、Prismaの user.id を使わないとリレーションが一致しない点に注意。
    firebaseUid は認証用、userId はDBリレーション用に明確に分けるのが◎。


次回やること

  • 学習イベントの保存
     - action, lang, wordId を記録
     - 例:{ action: "learn", lang: "ja", wordId: "xxxx" }

  • Firebaseセッションの安定化
     - Cookieの有効期限切れに備え、認証ロジックを再確認・修正

  • UI/UX改善
     - ダッシュボードとエディタのデザイン統一
     - スマホ表示での余白や配置調整

Discussion