實作創意題與資料庫整合

This commit is contained in:
2025-09-29 03:24:17 +08:00
parent aa34d2d078
commit a3b46b4430
18 changed files with 1366 additions and 30 deletions

View File

@@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from 'next/server'
import { getCreativeTestAnswersByTestResultId } from '@/lib/database/models/creative_test_answer'
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const testResultId = searchParams.get('testResultId')
if (!testResultId) {
return NextResponse.json(
{ success: false, error: '缺少測試結果ID' },
{ status: 400 }
)
}
// 獲取創意測驗答案
const answers = await getCreativeTestAnswersByTestResultId(testResultId)
return NextResponse.json({
success: true,
data: answers
})
} catch (error) {
console.error('獲取創意測驗答案失敗:', error)
return NextResponse.json(
{ success: false, error: '伺服器錯誤' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,155 @@
import { NextRequest, NextResponse } from 'next/server'
import { createTestResult, getTestResultsByUserId } from '@/lib/database/models/test_result'
import { createCreativeTestAnswers } from '@/lib/database/models/creative_test_answer'
import { getAllCreativeQuestions } from '@/lib/database/models/creative_question'
export async function POST(request: NextRequest) {
let body: any
try {
body = await request.json()
const {
userId,
answers,
completedAt
} = body
// 驗證必要欄位
if (!userId || !answers || !completedAt) {
return NextResponse.json(
{ success: false, error: '缺少必要欄位' },
{ status: 400 }
)
}
// 獲取創意題目
const questions = await getAllCreativeQuestions()
if (questions.length === 0) {
return NextResponse.json(
{ success: false, error: '無法獲取題目' },
{ status: 500 }
)
}
// 計算分數(包含反向題處理)
let totalScore = 0
const answerRecords = []
for (let i = 0; i < questions.length; i++) {
const question = questions[i]
const userAnswer = answers[i] || 1 // 預設為1
// 處理反向題:如果是反向題,分數要反轉
let score = userAnswer
if (question.is_reverse) {
score = 6 - userAnswer // 5->1, 4->2, 3->3, 2->4, 1->5
}
totalScore += score
answerRecords.push({
test_result_id: '', // 稍後填入
question_id: question.id,
user_answer: userAnswer,
score: score
})
}
// 計算百分比分數
const maxPossibleScore = questions.length * 5 // 每題最高5分
const scorePercentage = Math.round((totalScore / maxPossibleScore) * 100)
// 建立測試結果
console.log('🔄 開始建立創意測驗結果...')
console.log('測試結果數據:', {
user_id: userId,
test_type: 'creative',
score: scorePercentage,
total_questions: questions.length,
correct_answers: totalScore, // 創意測驗用總分數代替正確答案數
completed_at: completedAt
})
const testResult = await createTestResult({
user_id: userId,
test_type: 'creative',
score: scorePercentage,
total_questions: questions.length,
correct_answers: totalScore,
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 createCreativeTestAnswers(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 creativeResults = results.filter(r => r.test_type === 'creative')
return NextResponse.json({
success: true,
data: creativeResults
})
} catch (error) {
console.error('獲取創意測驗結果失敗:', error)
return NextResponse.json(
{ success: false, error: '伺服器錯誤' },
{ status: 500 }
)
}
}

View File

@@ -8,6 +8,7 @@ import { Progress } from "@/components/ui/progress"
import { Lightbulb, Home, RotateCcw, TrendingUp } from "lucide-react"
import Link from "next/link"
import { creativeQuestions } from "@/lib/questions/creative-questions"
import { useAuth } from "@/lib/hooks/use-auth"
interface CreativeTestResults {
type: string
@@ -16,17 +17,139 @@ interface CreativeTestResults {
maxScore: number
answers: Record<number, number>
completedAt: string
dimensionScores?: {
innovation: { percentage: number, rawScore: number, maxScore: number }
imagination: { percentage: number, rawScore: number, maxScore: number }
flexibility: { percentage: number, rawScore: number, maxScore: number }
originality: { percentage: number, rawScore: number, maxScore: number }
}
}
export default function CreativeResultsPage() {
const { user } = useAuth()
const [results, setResults] = useState<CreativeTestResults | null>(null)
const [questions, setQuestions] = useState<any[]>([])
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
const loadData = async () => {
if (!user) return
try {
// 從資料庫獲取最新的創意測驗結果
const response = await fetch(`/api/test-results/creative?userId=${user.id}`)
const data = await response.json()
if (data.success && data.data.length > 0) {
// 取最新的結果
const latestResult = data.data[0]
// 獲取題目資料來計算各維度分數
const questionsResponse = await fetch('/api/creative-questions')
const questionsData = await questionsResponse.json()
if (questionsData.success) {
setQuestions(questionsData.questions)
// 計算各維度分數
const dimensionScores = await calculateDimensionScores(latestResult, questionsData.questions)
setResults({
type: "creative",
score: latestResult.score,
totalScore: latestResult.correct_answers,
maxScore: latestResult.total_questions * 5,
answers: {}, // 從資料庫結果中獲取
completedAt: latestResult.completed_at,
dimensionScores: dimensionScores
})
}
} else {
// 如果沒有資料庫結果,回退到 localStorage
const savedResults = localStorage.getItem("creativeTestResults")
if (savedResults) {
setResults(JSON.parse(savedResults))
}
}
} catch (error) {
console.error('Error loading creative test results:', error)
// 回退到 localStorage
const savedResults = localStorage.getItem("creativeTestResults")
if (savedResults) {
setResults(JSON.parse(savedResults))
}
}, [])
} finally {
setIsLoading(false)
}
}
loadData()
}, [user])
// 計算各維度分數
const calculateDimensionScores = async (testResult: any, questions: any[]) => {
try {
// 獲取詳細答案
const answersResponse = await fetch(`/api/creative-test-answers?testResultId=${testResult.id}`)
const answersData = await answersResponse.json()
if (!answersData.success) {
return {
innovation: { percentage: 0, rawScore: 0, maxScore: 0 },
imagination: { percentage: 0, rawScore: 0, maxScore: 0 },
flexibility: { percentage: 0, rawScore: 0, maxScore: 0 },
originality: { percentage: 0, rawScore: 0, maxScore: 0 }
}
}
const answers = answersData.data
const dimensionScores: Record<string, { total: number, count: number }> = {
innovation: { total: 0, count: 0 },
imagination: { total: 0, count: 0 },
flexibility: { total: 0, count: 0 },
originality: { total: 0, count: 0 }
}
// 計算各維度分數
answers.forEach((answer: any) => {
const question = questions.find(q => q.id === answer.question_id)
if (question && dimensionScores[question.category]) {
dimensionScores[question.category].total += answer.score
dimensionScores[question.category].count += 1
}
})
// 計算百分比分數和原始分數
const result = {
innovation: { percentage: 0, rawScore: 0, maxScore: 0 },
imagination: { percentage: 0, rawScore: 0, maxScore: 0 },
flexibility: { percentage: 0, rawScore: 0, maxScore: 0 },
originality: { percentage: 0, rawScore: 0, maxScore: 0 }
}
Object.keys(dimensionScores).forEach(category => {
const { total, count } = dimensionScores[category]
const maxScore = count * 5
const percentage = count > 0 ? Math.round((total / maxScore) * 100) : 0
result[category as keyof typeof result] = {
percentage: percentage,
rawScore: total,
maxScore: maxScore
}
})
return result
} catch (error) {
console.error('計算維度分數失敗:', error)
return {
innovation: { percentage: 0, rawScore: 0, maxScore: 0 },
imagination: { percentage: 0, rawScore: 0, maxScore: 0 },
flexibility: { percentage: 0, rawScore: 0, maxScore: 0 },
originality: { percentage: 0, rawScore: 0, maxScore: 0 }
}
}
}
if (!results) {
return (
@@ -78,7 +201,33 @@ export default function CreativeResultsPage() {
const creativityLevel = getCreativityLevel(results.score)
// Calculate category scores
// Calculate category scores - prioritize database data if available
let categoryResults: Array<{
category: string
name: string
score: number
rawScore: number
maxRawScore: number
}> = []
if (results.dimensionScores) {
// Use database-calculated dimension scores
const dimensionNames = {
innovation: '創新能力',
imagination: '想像力',
flexibility: '靈活性',
originality: '原創性'
}
categoryResults = Object.entries(results.dimensionScores).map(([key, data]) => ({
category: key,
name: dimensionNames[key as keyof typeof dimensionNames],
score: data.percentage,
rawScore: data.rawScore,
maxRawScore: data.maxScore
}))
} else {
// Fallback to localStorage calculation
const categoryScores = {
innovation: { total: 0, count: 0, name: "創新能力" },
imagination: { total: 0, count: 0, name: "想像力" },
@@ -93,13 +242,14 @@ export default function CreativeResultsPage() {
categoryScores[question.category].count += 1
})
const categoryResults = Object.entries(categoryScores).map(([key, data]) => ({
categoryResults = Object.entries(categoryScores).map(([key, data]) => ({
category: key,
name: data.name,
score: data.count > 0 ? Math.round((data.total / (data.count * 5)) * 100) : 0,
rawScore: data.total,
maxRawScore: data.count * 5,
}))
}
return (
<div className="min-h-screen bg-background">
@@ -338,11 +488,11 @@ export default function CreativeResultsPage() {
{/* Legend */}
<div className="flex justify-center">
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-xs">
{categoryResults.map((category) => (
{categoryResults.map((category) => (
<div key={category.category} className="flex items-center gap-2">
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
<span className="text-muted-foreground">{category.name}</span>
</div>
</div>
))}
</div>
</div>

View File

@@ -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 CreativeQuestion {
id: number
@@ -18,11 +19,13 @@ interface CreativeQuestion {
export default function CreativeTestPage() {
const router = useRouter()
const { user } = useAuth()
const [questions, setQuestions] = useState<CreativeQuestion[]>([])
const [currentQuestion, setCurrentQuestion] = useState(0)
const [answers, setAnswers] = useState<Record<number, number>>({})
const [timeRemaining, setTimeRemaining] = useState(30 * 60) // 30 minutes in seconds
const [isLoading, setIsLoading] = useState(true)
const [isSubmitting, setIsSubmitting] = useState(false)
// Load questions from database
useEffect(() => {
@@ -88,31 +91,83 @@ export default function CreativeTestPage() {
}
}
const handleSubmit = () => {
// Calculate score based on creativity scoring
let totalScore = 0
questions.forEach((question, index) => {
const answer = answers[index] || 1
// For creativity, higher scores indicate more creative thinking
// 反向題:選擇 5 得 1 分,選擇 1 得 5 分
totalScore += question.is_reverse ? 6 - answer : answer
})
const handleSubmit = async () => {
console.log('🔍 開始提交創意測驗...')
console.log('用戶狀態:', user)
const maxScore = questions.length * 5
const score = Math.round((totalScore / maxScore) * 100)
// Store results in localStorage
const results = {
type: "creative",
score,
totalScore,
maxScore,
answers,
completedAt: new Date().toISOString(),
if (!user) {
console.log('❌ 用戶未登入')
alert('請先登入')
return
}
localStorage.setItem("creativeTestResults", JSON.stringify(results))
router.push("/results/creative")
console.log('✅ 用戶已登入用戶ID:', user.id)
setIsSubmitting(true)
try {
// Calculate score based on creativity scoring
let totalScore = 0
questions.forEach((question, index) => {
const answer = answers[index] || 1
// For creativity, higher scores indicate more creative thinking
// 反向題:選擇 5 得 1 分,選擇 1 得 5 分
totalScore += question.is_reverse ? 6 - answer : answer
})
const maxScore = questions.length * 5
const score = Math.round((totalScore / maxScore) * 100)
// Store results in localStorage (for backward compatibility)
const results = {
type: "creative",
score,
totalScore,
maxScore,
answers,
completedAt: new Date().toISOString(),
}
localStorage.setItem("creativeTestResults", JSON.stringify(results))
console.log('✅ 結果已儲存到 localStorage')
// Upload to database
console.log('🔄 開始上傳到資料庫...')
const uploadData = {
userId: user.id,
answers: Object.values(answers),
completedAt: new Date().toISOString().replace('Z', '').replace('T', ' ')
}
console.log('上傳數據:', uploadData)
const uploadResponse = await fetch('/api/test-results/creative', {
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/creative")
} catch (error) {
console.error('❌ 提交測驗失敗:', error)
alert('提交測驗失敗,請重試')
} finally {
setIsSubmitting(false)
}
}
if (isLoading) {
@@ -223,8 +278,8 @@ export default function CreativeTestPage() {
</div>
{isLastQuestion ? (
<Button onClick={handleSubmit} disabled={!hasAnswer} className="bg-green-600 hover:bg-green-700">
<Button onClick={handleSubmit} disabled={!hasAnswer || isSubmitting} className="bg-green-600 hover:bg-green-700">
{isSubmitting ? '提交中...' : '提交測試'}
</Button>
) : (
<Button onClick={handleNext} disabled={!hasAnswer}>