diff --git a/app/api/upload/route.ts b/app/api/upload/route.ts new file mode 100644 index 0000000..9a7bc2d --- /dev/null +++ b/app/api/upload/route.ts @@ -0,0 +1,140 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { writeFile, mkdir } from 'fs/promises'; +import { ProjectService, ProjectFileService, CriteriaItemService } from '@/lib/services/database'; +import { + getUploadAbsolutePath, + getUploadRelativePath, + getUploadDirPath, + generateUniqueFileName, + isValidFileType, + isValidFileSize +} from '@/lib/utils/file-path'; + +export async function POST(request: NextRequest) { + try { + const formData = await request.formData(); + const projectTitle = formData.get('projectTitle') as string; + const projectDescription = formData.get('projectDescription') as string; + const file = formData.get('file') as File; + + console.log('🚀 開始處理檔案上傳...'); + console.log('📝 專案標題:', projectTitle); + console.log('📋 專案描述:', projectDescription); + console.log('📁 上傳文件:', file ? file.name : '無'); + + // 驗證必填欄位 + if (!projectTitle?.trim()) { + return NextResponse.json( + { success: false, error: '請填寫專案標題' }, + { status: 400 } + ); + } + + if (!file) { + return NextResponse.json( + { success: false, error: '請上傳文件' }, + { status: 400 } + ); + } + + // 驗證檔案類型 + if (!isValidFileType(file.type)) { + return NextResponse.json( + { success: false, error: '不支援的檔案類型' }, + { status: 400 } + ); + } + + // 驗證檔案大小(100MB) + if (!isValidFileSize(file.size, 100)) { + return NextResponse.json( + { success: false, error: '檔案大小超過 100MB 限制' }, + { status: 400 } + ); + } + + // 獲取預設評分標準模板 + const templates = await CriteriaItemService.getAllTemplates(); + if (!templates || templates.length === 0) { + return NextResponse.json( + { success: false, error: '未找到評分標準,請先設定評分標準' }, + { status: 400 } + ); + } + + const templateId = templates[0].id; + console.log('📊 使用評分標準模板 ID:', templateId); + + // 創建專案記錄 + const projectData = { + user_id: 1, // 暫時使用固定用戶 ID + template_id: templateId, + title: projectTitle, + description: projectDescription || null, + status: 'uploading' as const, // 初始狀態為上傳中 + analysis_started_at: null, + analysis_completed_at: null + }; + + console.log('💾 創建專案記錄...'); + const projectResult = await ProjectService.create(projectData); + const projectId = (projectResult as any).insertId; + console.log('✅ 專案記錄創建成功,ID:', projectId); + + // 準備檔案儲存 + const uploadDir = getUploadDirPath(projectId); + await mkdir(uploadDir, { recursive: true }); + + // 生成唯一檔案名稱 + const uniqueFileName = generateUniqueFileName(file.name); + const filePath = getUploadAbsolutePath(projectId, uniqueFileName); + const relativeFilePath = getUploadRelativePath(projectId, uniqueFileName); + + // 儲存檔案 + console.log('💾 儲存檔案到:', filePath); + console.log('📁 相對路徑:', relativeFilePath); + const bytes = await file.arrayBuffer(); + await writeFile(filePath, Buffer.from(bytes)); + + // 創建檔案記錄 + const fileExtension = file.name.split('.').pop(); + const fileData = { + project_id: projectId, + original_name: file.name, + file_name: uniqueFileName, + file_path: relativeFilePath, // 使用相對路徑 + file_size: file.size, + file_type: fileExtension || '', + mime_type: file.type, + upload_status: 'completed' as const, + upload_progress: 100 + }; + + console.log('💾 創建檔案記錄...'); + await ProjectFileService.create(fileData); + console.log('✅ 檔案記錄創建成功'); + + // 更新專案狀態為 completed(檔案上傳完成) + console.log('🔄 更新專案狀態為 completed...'); + await ProjectService.update(projectId, { status: 'completed' }); + console.log('✅ 專案狀態更新完成'); + + return NextResponse.json({ + success: true, + data: { + projectId, + fileName: file.name, + fileSize: file.size, + fileType: file.type + }, + message: '檔案上傳成功' + }); + + } catch (error) { + console.error('❌ 檔案上傳失敗:', error); + return NextResponse.json( + { success: false, error: '檔案上傳失敗,請稍後再試' }, + { status: 500 } + ); + } +} diff --git a/app/upload/page.tsx b/app/upload/page.tsx index 28e92d3..538444a 100644 --- a/app/upload/page.tsx +++ b/app/upload/page.tsx @@ -135,25 +135,56 @@ export default function UploadPage() { setIsAnalyzing(true) try { - console.log('🚀 開始 AI 評審流程...') + console.log('🚀 開始上傳和評審流程...') console.log('📝 專案標題:', projectTitle) console.log('📋 專案描述:', projectDescription) console.log('📁 上傳文件數量:', files.length) console.log('🌐 網站連結:', websiteUrl) - // 準備表單數據 + let projectId = null; + + // 如果有文件,先上傳到資料庫 + if (files.length > 0) { + console.log('📤 上傳文件到資料庫...') + const firstFile = files[0] + if (firstFile.file) { + const uploadFormData = new FormData() + uploadFormData.append('projectTitle', projectTitle) + uploadFormData.append('projectDescription', projectDescription) + uploadFormData.append('file', firstFile.file) + + const uploadResponse = await fetch('/api/upload', { + method: 'POST', + body: uploadFormData, + }) + + const uploadResult = await uploadResponse.json() + + if (uploadResult.success) { + projectId = uploadResult.data.projectId + console.log('✅ 文件上傳成功,專案 ID:', projectId) + toast({ + title: "文件上傳成功", + description: `專案已創建,ID: ${projectId}`, + }) + } else { + throw new Error(uploadResult.error || '文件上傳失敗') + } + } else { + throw new Error('文件對象遺失,請重新上傳') + } + } + + // 準備 AI 評審數據 const formData = new FormData() formData.append('projectTitle', projectTitle) formData.append('projectDescription', projectDescription) if (files.length > 0) { - // 只處理第一個文件(可以後續擴展支援多文件) const firstFile = files[0] if (firstFile.file) { formData.append('file', firstFile.file) console.log('📄 處理文件:', firstFile.name, '大小:', firstFile.size) - } else { - throw new Error('文件對象遺失,請重新上傳') } } @@ -162,8 +193,8 @@ export default function UploadPage() { console.log('🌐 處理網站連結:', websiteUrl) } - // 發送評審請求 - console.log('📤 發送評審請求到 API...') + // 發送 AI 評審請求 + console.log('🤖 開始 AI 評審...') const response = await fetch('/api/evaluate', { method: 'POST', body: formData, @@ -195,9 +226,9 @@ export default function UploadPage() { throw new Error(result.error || '評審失敗') } } catch (error) { - console.error('❌ AI 評審失敗:', error) + console.error('❌ 上傳或評審失敗:', error) toast({ - title: "評審失敗", + title: "操作失敗", description: error instanceof Error ? error.message : "請稍後再試", variant: "destructive", }) diff --git a/lib/utils/file-path.ts b/lib/utils/file-path.ts new file mode 100644 index 0000000..6e51586 --- /dev/null +++ b/lib/utils/file-path.ts @@ -0,0 +1,67 @@ +import { join } from 'path'; + +/** + * 檔案路徑工具函數 + * 用於處理檔案上傳和路徑管理 + */ + +/** + * 獲取檔案上傳的絕對路徑(用於實際儲存) + */ +export function getUploadAbsolutePath(projectId: string | number, fileName: string): string { + return join(process.cwd(), 'uploads', 'projects', projectId.toString(), fileName); +} + +/** + * 獲取檔案上傳的相對路徑(用於資料庫儲存) + */ +export function getUploadRelativePath(projectId: string | number, fileName: string): string { + return `uploads/projects/${projectId}/${fileName}`; +} + +/** + * 獲取檔案上傳目錄的絕對路徑 + */ +export function getUploadDirPath(projectId: string | number): string { + return join(process.cwd(), 'uploads', 'projects', projectId.toString()); +} + +/** + * 從相對路徑獲取絕對路徑 + */ +export function getAbsolutePathFromRelative(relativePath: string): string { + return join(process.cwd(), relativePath); +} + +/** + * 生成唯一檔案名稱 + */ +export function generateUniqueFileName(originalName: string): string { + const fileExtension = originalName.split('.').pop(); + const timestamp = Date.now(); + const randomString = Math.random().toString(36).substr(2, 9); + return `${timestamp}_${randomString}.${fileExtension}`; +} + +/** + * 驗證檔案類型 + */ +export function isValidFileType(mimeType: string): boolean { + const allowedTypes = [ + 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'video/mp4', + 'video/avi', + 'video/quicktime', + 'application/pdf' + ]; + return allowedTypes.includes(mimeType); +} + +/** + * 驗證檔案大小 + */ +export function isValidFileSize(size: number, maxSizeMB: number = 100): boolean { + const maxSize = maxSizeMB * 1024 * 1024; // 轉換為位元組 + return size <= maxSize; +} diff --git a/uploads/projects/1/1758622832603_gwou09ew5.pptx b/uploads/projects/1/1758622832603_gwou09ew5.pptx new file mode 100644 index 0000000..8b38ba6 Binary files /dev/null and b/uploads/projects/1/1758622832603_gwou09ew5.pptx differ diff --git a/uploads/projects/2/1758623148684_oaa90wfdy.pptx b/uploads/projects/2/1758623148684_oaa90wfdy.pptx new file mode 100644 index 0000000..8b38ba6 Binary files /dev/null and b/uploads/projects/2/1758623148684_oaa90wfdy.pptx differ