資格試験の勉強で一番もったいないのは、「なんとなく全範囲を繰り返す」という勉強法です。得意な分野を何度も復習する一方で、本当に詰まっている箇所が放置される——そのパターンにハマって、試験直前に「あの分野が弱かった」と気づく経験をした方も多いのではないでしょうか。
ここではRork と Gemini API を使って、その問題を解決する AI 資格試験対策アプリを作ります。ユーザーが問題を解くたびに AI が苦手分野を特定し、次に何を学ぶべきかを自動で提案するアダプティブ学習の仕組みを実装します。
完成するアプリは、以下の機能を備えています。
- カテゴリ別の問題演習(4択形式)
- 解答ログをもとにした苦手分野の自動分析(Gemini API)
- 弱点に特化した問題の優先出題
- 学習進捗の可視化
- Supabase による解答データの永続化
対象読者は、Rork でアプリを作ったことがある方(入門記事の内容は把握済み)で、AI 連携に挑戦してみたい方を想定しています。
アプリ設計:何をどう作るか
まず完成形のイメージを決めてから実装に入ります。アダプティブ学習アプリの設計で重要なのは「データモデル」です。問題に答えるたびに蓄積されるデータをどう使うかで、AIの精度が決まります。
今回のデータ構造はシンプルにしました。
users ← ユーザー情報
questions ← 問題バンク(カテゴリ・正解・解説)
answer_logs ← 各ユーザーの解答履歴(問題ID・正誤・回答時間)
weak_analysis ← AIが生成した苦手分析結果(定期更新)
Supabase の PostgreSQL にこのテーブルを用意し、Rork からリアルタイムで読み書きします。
Rork で新しいプロジェクトを作成し、最初のプロンプトで構造を伝えましょう。
React Native アプリを作ってください。
資格試験対策アプリで、以下の機能が必要です。
- ホーム画面:本日の学習目標、弱点分野サマリー
- 問題演習画面:4択問題、解答後に解説表示
- 分析画面:AI による苦手分野のレポート
- 設定画面:目標試験日・1日の問題数
Supabase と Gemini API を使います。
デザインは清潔感のある白ベースで、
達成感が伝わるUIにしてください。
このプロンプトで Rork が生成する初期コードを確認したら、次の実装ステップに進みます。
Supabase のセットアップ:学習データの設計
Supabase でプロジェクトを作成し、以下の SQL でテーブルを作ります。
-- 問題テーブル
CREATE TABLE questions (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
category TEXT NOT NULL, -- 例: 'ネットワーク', 'セキュリティ'
difficulty INTEGER DEFAULT 1, -- 1:易 2:中 3:難
question_text TEXT NOT NULL,
options JSONB NOT NULL, -- ["選択肢A", "選択肢B", "選択肢C", "選択肢D"]
correct_index INTEGER NOT NULL, -- 0-3 の正解インデックス
explanation TEXT, -- 解説文
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 解答ログテーブル
CREATE TABLE answer_logs (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL,
question_id UUID REFERENCES questions(id),
selected_index INTEGER NOT NULL,
is_correct BOOLEAN NOT NULL,
time_spent_seconds INTEGER, -- 解答にかかった秒数
answered_at TIMESTAMPTZ DEFAULT NOW()
);
-- 弱点分析テーブル
CREATE TABLE weak_analysis (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL,
analyzed_at TIMESTAMPTZ DEFAULT NOW(),
weak_categories JSONB, -- [{"category": "ネットワーク", "accuracy": 0.4, ...}]
ai_suggestion TEXT, -- Gemini が生成したアドバイス文
next_question_ids UUID[] -- 優先的に出題すべき問題ID
);Row Level Security(RLS)を有効にして、ユーザーが自分のデータだけにアクセスできるように設定するのを忘れないでください。
ALTER TABLE answer_logs ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users see own logs"
ON answer_logs FOR ALL
USING (user_id = auth.uid());問題演習画面の実装
問題画面は、アプリの核心部分です。Rork に以下のプロンプトで実装を依頼します。
QuestionScreen.tsx を実装してください。
処理の流れ:
1. Supabase から問題を1問取得
(弱点分析データがある場合は weak_analysis.next_question_ids を優先)
2. 問題文と4択選択肢を表示
3. 選択肢タップで解答、正誤を即座にフィードバック
4. 「解説を見る」ボタンで explanation を表示
5. 「次の問題へ」で answer_logs に記録して次問題へ
タイマー機能も入れてください。
解答にかかった秒数を time_spent_seconds として記録します。
Rork が生成したコードを見ると、多くの場合、問題取得のロジックが単純な SELECT * FROM questions LIMIT 1 になっています。ここを改善して、弱点分野の問題が優先されるようにします。
// src/lib/questionSelector.ts
import { supabase } from './supabase';
export async function getNextQuestion(userId: string) {
// まず弱点分析データを確認
const { data: analysis } = await supabase
.from('weak_analysis')
.select('next_question_ids, weak_categories')
.eq('user_id', userId)
.order('analyzed_at', { ascending: false })
.limit(1)
.single();
if (analysis?.next_question_ids?.length > 0) {
// 優先問題リストから未回答のものを取得
const { data: priorityQuestion } = await supabase
.from('questions')
.select('*')
.in('id', analysis.next_question_ids)
.not('id', 'in', await getRecentlyAnswered(userId)) // 直近回答済みを除く
.order('difficulty', { ascending: true }) // 易しいものから
.limit(1)
.single();
if (priorityQuestion) return priorityQuestion;
}
// 弱点データがない場合はランダム(ただし直近の問題は除く)
const { data: randomQuestion } = await supabase
.from('questions')
.select('*')
.not('id', 'in', await getRecentlyAnswered(userId))
.order('id') // 疑似ランダム
.limit(10)
.then(({ data }) => ({ data: data?.[Math.floor(Math.random() * (data?.length ?? 1))] }));
return randomQuestion;
}
async function getRecentlyAnswered(userId: string): Promise<string[]> {
// 直近24時間に回答した問題IDを返す(同じ問題の連続出題を防ぐ)
const { data } = await supabase
.from('answer_logs')
.select('question_id')
.eq('user_id', userId)
.gte('answered_at', new Date(Date.now() - 86400000).toISOString());
return data?.map(d => d.question_id) ?? [];
}このコードで重要なのは「直近24時間に回答した問題は除く」という処理です。弱点問題の優先出題だけ実装すると、同じ問題が何度も出てきてしまいます。実際に動かしてみて気づいた点ですが、これがないとユーザー体験が著しく悪化します。
AI 苦手分析:Gemini API との連携
アプリの差別化ポイントになる、Gemini による苦手分析を実装します。問題を10問解くたびに自動で分析が走る設計にします。
// src/lib/weaknessAnalyzer.ts
const GEMINI_API_KEY = process.env.EXPO_PUBLIC_GEMINI_API_KEY; // YOUR_GEMINI_API_KEY
export async function analyzeWeakness(
userId: string,
answerLogs: AnswerLog[]
): Promise<void> {
// カテゴリ別の正答率を集計
const categoryStats = answerLogs.reduce((acc, log) => {
const cat = log.question.category;
if (!acc[cat]) acc[cat] = { correct: 0, total: 0, avgTime: 0 };
acc[cat].total++;
if (log.is_correct) acc[cat].correct++;
acc[cat].avgTime += log.time_spent_seconds;
return acc;
}, {} as Record<string, { correct: number; total: number; avgTime: number }>);
const statsText = Object.entries(categoryStats)
.map(([cat, s]) => {
const accuracy = (s.correct / s.total * 100).toFixed(0);
const avgTime = (s.avgTime / s.total).toFixed(0);
return `${cat}: 正答率${accuracy}%、平均${avgTime}秒`;
})
.join('\n');
// Gemini API に分析を依頼
const response = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_API_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{
parts: [{
text: `資格試験学習者の解答データを分析してください。
カテゴリ別成績:
${statsText}
以下の形式で JSON を返してください:
{
"weakest_category": "最も苦手なカテゴリ名",
"accuracy_map": {"カテゴリ名": 正答率(0-1)},
"advice": "100文字以内の学習アドバイス(です・ます調で)",
"focus_categories": ["優先的に学習すべきカテゴリ(最大3つ)"]
}`
}]
}],
generationConfig: {
responseMimeType: 'application/json',
temperature: 0.3
}
})
}
);
const result = await response.json();
const analysisData = JSON.parse(
result.candidates[0].content.parts[0].text
);
// 弱点分野に対応する問題を取得
const { data: weakQuestions } = await supabase
.from('questions')
.select('id')
.in('category', analysisData.focus_categories)
.order('difficulty', { ascending: true })
.limit(20);
// 分析結果を Supabase に保存
await supabase.from('weak_analysis').insert({
user_id: userId,
weak_categories: Object.entries(analysisData.accuracy_map).map(
([category, accuracy]) => ({ category, accuracy })
),
ai_suggestion: analysisData.advice,
next_question_ids: weakQuestions?.map(q => q.id) ?? []
});
}Gemini API の responseMimeType: 'application/json' を指定すると、JSON パースが確実に行えます。これを指定しないと、たまに Markdown のコードブロックで囲まれたテキストが返ってきて、パースに失敗することがあります。
学習進捗の可視化
分析画面では、ユーザーの学習状況をひと目で把握できる UI を実装します。
AnalysisScreen.tsx を実装してください。
表示内容:
- 総問題数と正答率(大きく表示)
- カテゴリ別の正答率をバーグラフで表示
(正答率が低いほど赤色、高いほど緑色)
- AI からのアドバイス文(カード形式)
- 「弱点問題を今すぐ解く」ボタン
- 過去7日間の学習日数カレンダー
データは Supabase の answer_logs と weak_analysis から取得します。
バーグラフの色分けは、Rork が生成するコードでは単色になりがちです。以下の補足プロンプトで改善を依頼できます。
カテゴリバーの色を正答率に応じて変えてください。
0-40%: #FF4444(赤)
41-70%: #FF8C00(オレンジ)
71-100%: #22C55E(緑)
色の変化に 300ms のアニメーションを入れてください。
収益化の設計:何を有料にするか
このアプリで収益化を考えるなら、無料・有料の境界線をどこに引くかが重要です。私が実際に使っているフレームワークは「基本的な学習体験は無料、継続・深化を助ける機能を有料にする」というものです。
無料で提供すること。
- 毎日10問の演習
- 基本的な正誤フィードバック
- カテゴリ別の正答率表示
有料(月額 ¥480 程度)にすること。
- 無制限の問題演習
- AI による詳細な苦手分析(今回実装したもの)
- 問題の難易度自動調整
- 学習リマインダーのプッシュ通知設定
AI 分析機能を有料にするのは、Gemini API の呼び出しコストがかかるためです。無料ユーザーへの AI 分析を全開放すると、ユーザーが増えるほど API コストが増大します。課金ユーザーのみに AI 機能を提供することで、コストと収益のバランスを保てます。
公開前にやること:問題データの準備
アプリ本体の実装ができても、問題データがなければリリースできません。最低でも 100 問は用意しておきましょう。
問題データの準備方法として、Gemini API を使って問題を自動生成するスクリプトを作っておくと便利です。
// scripts/generateQuestions.ts
// ローカルで実行して Supabase に一括インポートするスクリプト
const categories = ['ネットワーク', 'セキュリティ', 'データベース', 'プログラミング'];
for (const category of categories) {
const response = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key=${GEMINI_API_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{
parts: [{
text: `${category}に関する情報処理試験レベルの4択問題を10問作成してください。
JSON形式で返してください:
[{
"category": "${category}",
"difficulty": 1-3,
"question_text": "問題文",
"options": ["A", "B", "C", "D"],
"correct_index": 0-3,
"explanation": "解説(100文字以内)"
}]`
}]
}],
generationConfig: { responseMimeType: 'application/json' }
})
}
);
// ... Supabase に INSERT
}生成した問題は必ず人間がレビューしてから使うことをおすすめします。AI が生成する問題は、まれに正解が曖昧なものや、解説が不正確なものが含まれます。
全体を振り返って:次の一歩
今日やることは一つです。Supabase でテーブルを作って、Gemini API キーを取得することから始めてください。環境が整えば、Rork でのアプリ実装は意外と速く進みます。
アダプティブ学習のロジック自体はシンプルでも、「解答データを貯めて AI に分析させる」というサイクルを実装するだけで、単純な問題集アプリとは一線を画すプロダクトになります。試験対策アプリは継続的に使われるため、LTV(顧客生涯価値)が高く、サブスクリプションモデルとの相性も良い分野です。
Rork の AI アプリ開発についてより詳しく知りたい方には、Rork と Gemini AI を組み合わせたアプリ開発ガイドも参考にしてみてください。また、Supabase のセットアップについては Rork + Supabase Auth & Realtime ガイド をご覧ください。