實作邏輯題結果與資料庫整合
This commit is contained in:
27
app/api/test-db/route.ts
Normal file
27
app/api/test-db/route.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { testConnection } from '@/lib/database/connection'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const isConnected = await testConnection()
|
||||
|
||||
if (isConnected) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '資料庫連接正常'
|
||||
})
|
||||
} else {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '資料庫連接失敗'
|
||||
}, { status: 500 })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('資料庫測試失敗:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '資料庫測試失敗',
|
||||
error: error instanceof Error ? error.message : '未知錯誤'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
149
app/api/test-results/logic/route.ts
Normal file
149
app/api/test-results/logic/route.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { createTestResult, getTestResultsByUserId } from '@/lib/database/models/test_result'
|
||||
import { createLogicTestAnswers } from '@/lib/database/models/logic_test_answer'
|
||||
import { getAllLogicQuestions } from '@/lib/database/models/logic_question'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
userId,
|
||||
answers,
|
||||
completedAt
|
||||
} = body
|
||||
|
||||
// 驗證必要欄位
|
||||
if (!userId || !answers || !completedAt) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '缺少必要欄位' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// 獲取邏輯題目
|
||||
const questions = await getAllLogicQuestions()
|
||||
if (questions.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '無法獲取題目' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
// 計算分數
|
||||
let correctAnswers = 0
|
||||
const answerRecords = []
|
||||
|
||||
for (let i = 0; i < questions.length; i++) {
|
||||
const question = questions[i]
|
||||
const userAnswer = answers[i] || ''
|
||||
const isCorrect = userAnswer === question.correct_answer
|
||||
|
||||
if (isCorrect) {
|
||||
correctAnswers++
|
||||
}
|
||||
|
||||
answerRecords.push({
|
||||
test_result_id: '', // 稍後填入
|
||||
question_id: question.id,
|
||||
user_answer: userAnswer as 'A' | 'B' | 'C' | 'D' | 'E',
|
||||
is_correct: isCorrect
|
||||
})
|
||||
}
|
||||
|
||||
const score = Math.round((correctAnswers / questions.length) * 100)
|
||||
|
||||
// 建立測試結果
|
||||
console.log('🔄 開始建立測試結果...')
|
||||
console.log('測試結果數據:', {
|
||||
user_id: userId,
|
||||
test_type: 'logic',
|
||||
score: score,
|
||||
total_questions: questions.length,
|
||||
correct_answers: correctAnswers,
|
||||
completed_at: completedAt
|
||||
})
|
||||
|
||||
const testResult = await createTestResult({
|
||||
user_id: userId,
|
||||
test_type: 'logic',
|
||||
score: score,
|
||||
total_questions: questions.length,
|
||||
correct_answers: correctAnswers,
|
||||
completed_at: completedAt
|
||||
})
|
||||
|
||||
console.log('測試結果建立結果:', testResult)
|
||||
|
||||
if (!testResult) {
|
||||
console.error('❌ 建立測試結果失敗')
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '建立測試結果失敗' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
console.log('✅ 測試結果建立成功:', testResult.id)
|
||||
|
||||
// 更新答案記錄的 test_result_id
|
||||
answerRecords.forEach(record => {
|
||||
record.test_result_id = testResult.id
|
||||
})
|
||||
|
||||
// 建立答案記錄
|
||||
const answerResults = await createLogicTestAnswers(answerRecords)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
testResult,
|
||||
answerCount: answerResults.length
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('上傳邏輯測驗結果失敗:', error)
|
||||
console.error('錯誤詳情:', {
|
||||
message: error instanceof Error ? error.message : '未知錯誤',
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
body: body
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: '伺服器錯誤',
|
||||
details: error instanceof Error ? error.message : '未知錯誤'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const userId = searchParams.get('userId')
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '缺少用戶ID' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// 獲取用戶的邏輯測驗結果
|
||||
const results = await getTestResultsByUserId(userId)
|
||||
const logicResults = results.filter(result => result.test_type === 'logic')
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: logicResults
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('獲取邏輯測驗結果失敗:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '伺服器錯誤' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
123
app/api/user/test-results/route.ts
Normal file
123
app/api/user/test-results/route.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getTestResultsByUserId } from '@/lib/database/models/test_result'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const userId = searchParams.get('userId')
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '缺少用戶ID' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// 獲取用戶的所有測試結果
|
||||
const allResults = await getTestResultsByUserId(userId)
|
||||
|
||||
if (allResults.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
results: [],
|
||||
stats: {
|
||||
totalTests: 0,
|
||||
averageScore: 0,
|
||||
bestScore: 0,
|
||||
lastTestDate: null,
|
||||
testCounts: {
|
||||
logic: 0,
|
||||
creative: 0,
|
||||
combined: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 按測試類型分組,只保留每種類型的最新結果
|
||||
const latestResults = {
|
||||
logic: allResults.filter(r => r.test_type === 'logic').sort((a, b) =>
|
||||
new Date(b.completed_at).getTime() - new Date(a.completed_at).getTime()
|
||||
)[0],
|
||||
creative: allResults.filter(r => r.test_type === 'creative').sort((a, b) =>
|
||||
new Date(b.completed_at).getTime() - new Date(a.completed_at).getTime()
|
||||
)[0],
|
||||
combined: allResults.filter(r => r.test_type === 'combined').sort((a, b) =>
|
||||
new Date(b.completed_at).getTime() - new Date(a.completed_at).getTime()
|
||||
)[0]
|
||||
}
|
||||
|
||||
// 只保留有結果的測試類型
|
||||
const results = Object.values(latestResults).filter(result => result !== undefined)
|
||||
|
||||
// 計算統計數據
|
||||
// 完成測試:基於每種類型測試是否有最新結果
|
||||
const totalTests = Object.values(latestResults).filter(result => result !== undefined).length
|
||||
|
||||
// 平均分數:基於每種類型測試的最新分數計算
|
||||
const latestScores = Object.values(latestResults)
|
||||
.filter(result => result !== undefined)
|
||||
.map(result => result.score)
|
||||
|
||||
const averageScore = latestScores.length > 0
|
||||
? Math.round(latestScores.reduce((sum, score) => sum + score, 0) / latestScores.length)
|
||||
: 0
|
||||
|
||||
// 最高分數:基於每種類型測試的最新分數
|
||||
const bestScore = latestScores.length > 0 ? Math.max(...latestScores) : 0
|
||||
|
||||
// 最近測試日期:基於所有測試結果
|
||||
const lastTestDate = allResults.length > 0
|
||||
? allResults.sort((a, b) =>
|
||||
new Date(b.completed_at).getTime() - new Date(a.completed_at).getTime()
|
||||
)[0]?.completed_at || null
|
||||
: null
|
||||
|
||||
// 計算各類型測試次數
|
||||
const testCounts = {
|
||||
logic: allResults.filter(r => r.test_type === 'logic').length,
|
||||
creative: allResults.filter(r => r.test_type === 'creative').length,
|
||||
combined: allResults.filter(r => r.test_type === 'combined').length
|
||||
}
|
||||
|
||||
// 轉換為前端需要的格式
|
||||
const formattedResults = results.map(result => ({
|
||||
type: result.test_type,
|
||||
score: result.score,
|
||||
completedAt: result.completed_at,
|
||||
testCount: testCounts[result.test_type as keyof typeof testCounts],
|
||||
details: {
|
||||
id: result.id,
|
||||
total_questions: result.total_questions,
|
||||
correct_answers: result.correct_answers,
|
||||
created_at: result.created_at
|
||||
}
|
||||
}))
|
||||
|
||||
// 按完成時間排序(最新的在前)
|
||||
formattedResults.sort((a, b) => new Date(b.completedAt).getTime() - new Date(a.completedAt).getTime())
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
results: formattedResults,
|
||||
stats: {
|
||||
totalTests,
|
||||
averageScore,
|
||||
bestScore,
|
||||
lastTestDate,
|
||||
testCounts
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('獲取用戶測試結果失敗:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '伺服器錯誤' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
@@ -204,7 +204,7 @@ export default function CombinedResultsPage() {
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">綜合能力測試結果</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
完成時間:{new Date(results.completedAt).toLocaleString("zh-TW")}
|
||||
完成時間:{new Date(results.completedAt).toLocaleString("zh-TW", { timeZone: "Asia/Taipei" })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -113,7 +113,7 @@ export default function CreativeResultsPage() {
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">創意能力測試結果</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
完成時間:{new Date(results.completedAt).toLocaleString("zh-TW")}
|
||||
完成時間:{new Date(results.completedAt).toLocaleString("zh-TW", { timeZone: "Asia/Taipei" })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -138,7 +138,7 @@ export default function LogicResultsPage() {
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">邏輯思維測試結果</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
完成時間:{new Date(results.completedAt).toLocaleString("zh-TW")}
|
||||
完成時間:{new Date(results.completedAt).toLocaleString("zh-TW", { timeZone: "Asia/Taipei" })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -14,6 +14,7 @@ interface TestResult {
|
||||
type: "logic" | "creative" | "combined"
|
||||
score: number
|
||||
completedAt: string
|
||||
testCount?: number
|
||||
details?: any
|
||||
}
|
||||
|
||||
@@ -33,66 +34,34 @@ function ResultsContent() {
|
||||
averageScore: 0,
|
||||
bestScore: 0,
|
||||
lastTestDate: null as string | null,
|
||||
testCounts: {
|
||||
logic: 0,
|
||||
creative: 0,
|
||||
combined: 0
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// Load all test results from localStorage
|
||||
const logicResults = localStorage.getItem("logicTestResults")
|
||||
const creativeResults = localStorage.getItem("creativeTestResults")
|
||||
const combinedResults = localStorage.getItem("combinedTestResults")
|
||||
const loadUserResults = async () => {
|
||||
if (!user) return
|
||||
|
||||
const allResults: TestResult[] = []
|
||||
try {
|
||||
const response = await fetch(`/api/user/test-results?userId=${user.id}`)
|
||||
const data = await response.json()
|
||||
|
||||
if (logicResults) {
|
||||
const data = JSON.parse(logicResults)
|
||||
allResults.push({
|
||||
type: "logic",
|
||||
score: data.score,
|
||||
completedAt: data.completedAt,
|
||||
details: data,
|
||||
})
|
||||
if (data.success) {
|
||||
setResults(data.data.results)
|
||||
setStats(data.data.stats)
|
||||
} else {
|
||||
console.error('Failed to load user results:', data.error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading user results:', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (creativeResults) {
|
||||
const data = JSON.parse(creativeResults)
|
||||
allResults.push({
|
||||
type: "creative",
|
||||
score: data.score,
|
||||
completedAt: data.completedAt,
|
||||
details: data,
|
||||
})
|
||||
}
|
||||
|
||||
if (combinedResults) {
|
||||
const data = JSON.parse(combinedResults)
|
||||
allResults.push({
|
||||
type: "combined",
|
||||
score: data.overallScore,
|
||||
completedAt: data.completedAt,
|
||||
details: data,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort by completion date (newest first)
|
||||
allResults.sort((a, b) => new Date(b.completedAt).getTime() - new Date(a.completedAt).getTime())
|
||||
|
||||
setResults(allResults)
|
||||
|
||||
// Calculate statistics
|
||||
if (allResults.length > 0) {
|
||||
const totalScore = allResults.reduce((sum, result) => sum + result.score, 0)
|
||||
const averageScore = Math.round(totalScore / allResults.length)
|
||||
const bestScore = Math.max(...allResults.map((r) => r.score))
|
||||
const lastTestDate = allResults[0].completedAt
|
||||
|
||||
setStats({
|
||||
totalTests: allResults.length,
|
||||
averageScore,
|
||||
bestScore,
|
||||
lastTestDate,
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
loadUserResults()
|
||||
}, [user])
|
||||
|
||||
const getTestTypeInfo = (type: string) => {
|
||||
switch (type) {
|
||||
@@ -263,7 +232,7 @@ function ResultsContent() {
|
||||
<Calendar className="w-6 h-6 text-accent" />
|
||||
</div>
|
||||
<div className="text-sm font-bold text-foreground mb-1">
|
||||
{stats.lastTestDate ? new Date(stats.lastTestDate).toLocaleDateString("zh-TW") : "無"}
|
||||
{stats.lastTestDate ? new Date(stats.lastTestDate).toLocaleDateString("zh-TW", { timeZone: "Asia/Taipei" }) : "無"}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">最近測試</div>
|
||||
</CardContent>
|
||||
@@ -295,8 +264,13 @@ function ResultsContent() {
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="font-medium text-foreground">{testInfo.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
完成時間:{new Date(result.completedAt).toLocaleString("zh-TW")}
|
||||
完成時間:{new Date(result.completedAt).toLocaleString("zh-TW", { timeZone: "Asia/Taipei" })}
|
||||
</p>
|
||||
{result.testCount && result.testCount > 1 && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
已測驗 {result.testCount} 次
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@@ -290,11 +290,11 @@ export default function CombinedTestPage() {
|
||||
{phase === "logic"
|
||||
? // Logic question options
|
||||
[
|
||||
{ value: 'A', text: currentQ.option_a },
|
||||
{ value: 'B', text: currentQ.option_b },
|
||||
{ value: 'C', text: currentQ.option_c },
|
||||
{ value: 'D', text: currentQ.option_d },
|
||||
{ value: 'E', text: currentQ.option_e }
|
||||
{ value: 'A', text: (currentQ as LogicQuestion).option_a },
|
||||
{ value: 'B', text: (currentQ as LogicQuestion).option_b },
|
||||
{ value: 'C', text: (currentQ as LogicQuestion).option_c },
|
||||
{ value: 'D', text: (currentQ as LogicQuestion).option_d },
|
||||
{ value: 'E', text: (currentQ as LogicQuestion).option_e }
|
||||
].map((option, index) => (
|
||||
<div
|
||||
key={index}
|
||||
|
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useAuth } from "@/lib/hooks/use-auth"
|
||||
|
||||
interface LogicQuestion {
|
||||
id: number
|
||||
@@ -23,11 +24,13 @@ interface LogicQuestion {
|
||||
|
||||
export default function LogicTestPage() {
|
||||
const router = useRouter()
|
||||
const { user } = useAuth()
|
||||
const [questions, setQuestions] = useState<LogicQuestion[]>([])
|
||||
const [currentQuestion, setCurrentQuestion] = useState(0)
|
||||
const [answers, setAnswers] = useState<Record<number, string>>({})
|
||||
const [timeRemaining, setTimeRemaining] = useState(20 * 60) // 20 minutes in seconds
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
// Load questions from database
|
||||
useEffect(() => {
|
||||
@@ -93,29 +96,88 @@ export default function LogicTestPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
// Calculate score
|
||||
let correctAnswers = 0
|
||||
questions.forEach((question, index) => {
|
||||
if (answers[index] === question.correct_answer) {
|
||||
correctAnswers++
|
||||
}
|
||||
})
|
||||
|
||||
const score = Math.round((correctAnswers / questions.length) * 100)
|
||||
|
||||
// Store results in localStorage
|
||||
const results = {
|
||||
type: "logic",
|
||||
score,
|
||||
correctAnswers,
|
||||
totalQuestions: questions.length,
|
||||
answers,
|
||||
completedAt: new Date().toISOString(),
|
||||
const handleSubmit = async () => {
|
||||
console.log('🔍 開始提交邏輯測驗...')
|
||||
console.log('用戶狀態:', user)
|
||||
|
||||
if (!user) {
|
||||
console.log('❌ 用戶未登入')
|
||||
alert('請先登入')
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.setItem("logicTestResults", JSON.stringify(results))
|
||||
router.push("/results/logic")
|
||||
console.log('✅ 用戶已登入,用戶ID:', user.id)
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
// Calculate score
|
||||
let correctAnswers = 0
|
||||
questions.forEach((question, index) => {
|
||||
if (answers[index] === question.correct_answer) {
|
||||
correctAnswers++
|
||||
}
|
||||
})
|
||||
|
||||
const score = Math.round((correctAnswers / questions.length) * 100)
|
||||
const completedAt = new Date().toISOString().replace('Z', '').replace('T', ' ')
|
||||
|
||||
console.log('📊 測驗結果計算:')
|
||||
console.log('答對題數:', correctAnswers)
|
||||
console.log('總題數:', questions.length)
|
||||
console.log('分數:', score)
|
||||
console.log('完成時間:', completedAt)
|
||||
|
||||
// Store results in localStorage (for backward compatibility)
|
||||
const results = {
|
||||
type: "logic",
|
||||
score,
|
||||
correctAnswers,
|
||||
totalQuestions: questions.length,
|
||||
answers,
|
||||
completedAt,
|
||||
}
|
||||
|
||||
localStorage.setItem("logicTestResults", JSON.stringify(results))
|
||||
console.log('✅ 結果已儲存到 localStorage')
|
||||
|
||||
// Upload to database
|
||||
console.log('🔄 開始上傳到資料庫...')
|
||||
const uploadData = {
|
||||
userId: user.id,
|
||||
answers: Object.values(answers),
|
||||
completedAt: completedAt
|
||||
}
|
||||
console.log('上傳數據:', uploadData)
|
||||
|
||||
const uploadResponse = await fetch('/api/test-results/logic', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(uploadData)
|
||||
})
|
||||
|
||||
console.log('📡 API 響應狀態:', uploadResponse.status)
|
||||
const uploadResult = await uploadResponse.json()
|
||||
console.log('📡 API 響應內容:', uploadResult)
|
||||
|
||||
if (uploadResult.success) {
|
||||
console.log('✅ 邏輯測驗結果已上傳到資料庫')
|
||||
console.log('測試結果ID:', uploadResult.data.testResult.id)
|
||||
console.log('答案記錄數量:', uploadResult.data.answerCount)
|
||||
} else {
|
||||
console.error('❌ 上傳到資料庫失敗:', uploadResult.error)
|
||||
// 即使上傳失敗,也繼續顯示結果
|
||||
}
|
||||
|
||||
router.push("/results/logic")
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 提交測驗失敗:', error)
|
||||
alert('提交測驗失敗,請重試')
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
@@ -204,11 +266,11 @@ export default function LogicTestPage() {
|
||||
{isLastQuestion ? (
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!hasAnswer}
|
||||
disabled={!hasAnswer || isSubmitting}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
size="sm"
|
||||
>
|
||||
提交測試
|
||||
{isSubmitting ? '提交中...' : '提交測試'}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleNext} disabled={!hasAnswer} size="sm">
|
||||
|
Reference in New Issue
Block a user