子ども向け学習アプリ開発ログ:学習APIの拡張 => 今日の単語・テスト単語・ダッシュボード連携
前回
- タグを登録し、選択中のタグを外せるようにすること
- UI デザインの再調整(ボタンの色・一覧性改善)
今回
-
学習関連API
- 今日の単語を10件, 学習済み単語からランダムに10件抽出(テスト用)
- 取得データをダッシュボードに反映
① 今日の単語(10件)
「今日の単語」では、まだ学習していない単語の中から10件を取得します。
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件を取得します。
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です)
StudyEvent
に action
条件を追加
getDashboardData()
内の studyEvent
クエリに action
条件を追加し、「学習完了(learn / quiz_end)」のみをカウント するように修正しました。
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