diff --git a/.gitignore b/.gitignore
index f650315..30ba2f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,4 +24,7 @@ yarn-error.log*
# typescript
*.tsbuildinfo
-next-env.d.ts
\ No newline at end of file
+next-env.d.ts
+
+# uploaded files
+/public/uploads/
\ No newline at end of file
diff --git a/app/api/admin/debug-competition-data/route.ts b/app/api/admin/debug-competition-data/route.ts
deleted file mode 100644
index 0fbb6c0..0000000
--- a/app/api/admin/debug-competition-data/route.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-// =====================================================
-// 調試競賽數據 API
-// =====================================================
-
-import { NextRequest, NextResponse } from 'next/server';
-import { DatabaseServiceBase } from '@/lib/services/database-service';
-
-export async function GET(request: NextRequest) {
- try {
- const { searchParams } = new URL(request.url);
- const competitionId = searchParams.get('competitionId') || '07e2303e-9647-11f0-b5d9-6e36c63cdb98';
-
- console.log('🔍 開始調試競賽數據...');
- console.log('競賽ID:', competitionId);
-
- // 1. 檢查競賽是否存在
- const competitionCheck = await DatabaseServiceBase.safeQuery(
- 'SELECT * FROM competitions WHERE id = ?',
- [competitionId]
- );
-
- console.log('📊 競賽檢查結果:', competitionCheck);
-
- // 2. 檢查競賽應用關聯
- const competitionAppsCheck = await DatabaseServiceBase.safeQuery(
- 'SELECT * FROM competition_apps WHERE competition_id = ?',
- [competitionId]
- );
-
- console.log('📊 競賽應用關聯檢查結果:', competitionAppsCheck);
-
- // 3. 檢查所有應用程式
- const allAppsCheck = await DatabaseServiceBase.safeQuery(
- 'SELECT id, name, is_active FROM apps WHERE is_active = 1 LIMIT 10',
- []
- );
-
- console.log('📊 所有應用程式檢查結果:', allAppsCheck);
-
- // 4. 檢查競賽規則
- const competitionRulesCheck = await DatabaseServiceBase.safeQuery(
- 'SELECT * FROM competition_rules WHERE competition_id = ?',
- [competitionId]
- );
-
- console.log('📊 競賽規則檢查結果:', competitionRulesCheck);
-
- // 5. 檢查評審
- const judgesCheck = await DatabaseServiceBase.safeQuery(
- 'SELECT id, name, title, department FROM judges WHERE is_active = 1 LIMIT 5',
- []
- );
-
- console.log('📊 評審檢查結果:', judgesCheck);
-
- return NextResponse.json({
- success: true,
- message: '調試數據獲取成功',
- data: {
- competition: competitionCheck,
- competitionApps: competitionAppsCheck,
- allApps: allAppsCheck,
- competitionRules: competitionRulesCheck,
- judges: judgesCheck
- }
- });
-
- } catch (error) {
- console.error('❌ 調試競賽數據失敗:', error);
- return NextResponse.json({
- success: false,
- message: '調試競賽數據失敗',
- error: error instanceof Error ? error.message : '未知錯誤'
- }, { status: 500 });
- }
-}
diff --git a/app/api/admin/force-cleanup/route.ts b/app/api/admin/force-cleanup/route.ts
deleted file mode 100644
index 456ee05..0000000
--- a/app/api/admin/force-cleanup/route.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-// =====================================================
-// 強制清理連線 API
-// =====================================================
-
-import { NextRequest, NextResponse } from 'next/server';
-import { db } from '@/lib/database';
-import { connectionMonitor } from '@/lib/connection-monitor';
-
-export async function POST(request: NextRequest) {
- try {
- console.log('🧹 開始強制清理資料庫連線...');
-
- // 獲取清理前的連線狀態
- const beforeStats = await connectionMonitor.getConnectionStats();
- console.log(`清理前連線數: ${beforeStats.activeConnections}`);
-
- // 強制關閉連線池
- try {
- await db.close();
- console.log('✅ 主要資料庫連線池已關閉');
- } catch (error) {
- console.error('❌ 關閉主要連線池失敗:', error);
- }
-
- // 等待一段時間讓連線完全關閉
- await new Promise(resolve => setTimeout(resolve, 2000));
-
- // 重新初始化連線池
- try {
- // 重新創建連線池實例
- const { Database } = await import('@/lib/database');
- const newDb = Database.getInstance();
- console.log('✅ 資料庫連線池已重新初始化');
- } catch (error) {
- console.error('❌ 重新初始化連線池失敗:', error);
- }
-
- // 獲取清理後的連線狀態
- const afterStats = await connectionMonitor.getConnectionStats();
- console.log(`清理後連線數: ${afterStats.activeConnections}`);
-
- return NextResponse.json({
- success: true,
- message: '強制清理完成',
- data: {
- before: {
- activeConnections: beforeStats.activeConnections,
- usagePercentage: beforeStats.usagePercentage
- },
- after: {
- activeConnections: afterStats.activeConnections,
- usagePercentage: afterStats.usagePercentage
- },
- cleaned: beforeStats.activeConnections - afterStats.activeConnections
- }
- });
-
- } catch (error) {
- console.error('強制清理失敗:', error);
- return NextResponse.json({
- success: false,
- message: '強制清理失敗',
- error: error instanceof Error ? error.message : '未知錯誤'
- }, { status: 500 });
- }
-}
diff --git a/app/api/admin/kill-connections/route.ts b/app/api/admin/kill-connections/route.ts
deleted file mode 100644
index a2eab49..0000000
--- a/app/api/admin/kill-connections/route.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-// =====================================================
-// 強制終止連線 API
-// =====================================================
-
-import { NextRequest, NextResponse } from 'next/server';
-import { db } from '@/lib/database';
-
-export async function POST(request: NextRequest) {
- try {
- console.log('💀 開始強制終止所有資料庫連線...');
-
- // 獲取所有 AI_Platform 的連線
- const connections = await db.query(`
- SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE
- FROM INFORMATION_SCHEMA.PROCESSLIST
- WHERE USER = 'AI_Platform' AND DB = 'db_AI_Platform'
- `);
-
- console.log(`找到 ${connections.length} 個 AI_Platform 連線`);
-
- const killedConnections = [];
-
- // 終止每個連線
- for (const conn of connections) {
- try {
- await db.query(`KILL CONNECTION ${conn.ID}`);
- killedConnections.push({
- id: conn.ID,
- host: conn.HOST,
- time: conn.TIME,
- command: conn.COMMAND
- });
- console.log(`✅ 已終止連線 ${conn.ID} (閒置 ${conn.TIME} 秒)`);
- } catch (error) {
- console.error(`❌ 終止連線 ${conn.ID} 失敗:`, error);
- }
- }
-
- // 等待連線完全關閉
- await new Promise(resolve => setTimeout(resolve, 2000));
-
- // 檢查剩餘連線
- const remainingConnections = await db.query(`
- SELECT COUNT(*) as count
- FROM INFORMATION_SCHEMA.PROCESSLIST
- WHERE USER = 'AI_Platform' AND DB = 'db_AI_Platform'
- `);
-
- const remainingCount = remainingConnections[0]?.count || 0;
-
- return NextResponse.json({
- success: true,
- message: '強制終止連線完成',
- data: {
- totalFound: connections.length,
- killed: killedConnections.length,
- remaining: remainingCount,
- killedConnections: killedConnections
- }
- });
-
- } catch (error) {
- console.error('強制終止連線失敗:', error);
- return NextResponse.json({
- success: false,
- message: '強制終止連線失敗',
- error: error instanceof Error ? error.message : '未知錯誤'
- }, { status: 500 });
- }
-}
diff --git a/app/api/test-connection/route.ts b/app/api/test-connection/route.ts
deleted file mode 100644
index 83cc7ea..0000000
--- a/app/api/test-connection/route.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-// =====================================================
-// 連線測試 API - 驗證連線釋放
-// =====================================================
-
-import { NextRequest, NextResponse } from 'next/server';
-import { connectionMonitor } from '@/lib/connection-monitor';
-
-export async function GET(request: NextRequest) {
- try {
- // 獲取測試前的連線狀態
- const beforeStats = await connectionMonitor.getConnectionStats();
-
- // 執行一些測試查詢
- const testQueries = [
- 'SELECT 1 as test1',
- 'SELECT 2 as test2',
- 'SELECT 3 as test3',
- 'SELECT COUNT(*) as user_count FROM users',
- 'SELECT COUNT(*) as app_count FROM apps'
- ];
-
- console.log('🧪 開始連線測試...');
- console.log(`測試前連線數: ${beforeStats.activeConnections}`);
-
- // 執行測試查詢
- for (let i = 0; i < testQueries.length; i++) {
- const { db } = await import('@/lib/database');
- await db.query(testQueries[i]);
- console.log(`✅ 完成測試查詢 ${i + 1}`);
- }
-
- // 等待一小段時間讓連線釋放
- await new Promise(resolve => setTimeout(resolve, 1000));
-
- // 獲取測試後的連線狀態
- const afterStats = await connectionMonitor.getConnectionStats();
-
- console.log(`測試後連線數: ${afterStats.activeConnections}`);
-
- return NextResponse.json({
- success: true,
- message: '連線測試完成',
- data: {
- before: {
- activeConnections: beforeStats.activeConnections,
- usagePercentage: beforeStats.usagePercentage
- },
- after: {
- activeConnections: afterStats.activeConnections,
- usagePercentage: afterStats.usagePercentage
- },
- difference: {
- connectionChange: afterStats.activeConnections - beforeStats.activeConnections,
- isReleased: afterStats.activeConnections <= beforeStats.activeConnections
- }
- }
- });
-
- } catch (error) {
- console.error('連線測試失敗:', error);
- return NextResponse.json({
- success: false,
- message: '連線測試失敗',
- error: error instanceof Error ? error.message : '未知錯誤'
- }, { status: 500 });
- }
-}
diff --git a/app/api/upload/avatar/route.ts b/app/api/upload/avatar/route.ts
new file mode 100644
index 0000000..ce07934
--- /dev/null
+++ b/app/api/upload/avatar/route.ts
@@ -0,0 +1,103 @@
+// =====================================================
+// 個人頭像上傳 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { writeFile, mkdir } from 'fs/promises';
+import { join } from 'path';
+import { UserService } from '@/lib/services/database-service';
+import { generateUniqueFileName, isValidImageType, isValidImageSize } from '@/lib/image-utils';
+import sharp from 'sharp';
+
+export async function POST(request: NextRequest) {
+ try {
+ const formData = await request.formData();
+ const file = formData.get('avatar') as File;
+ const userId = formData.get('userId') as string;
+
+ if (!file) {
+ return NextResponse.json(
+ { success: false, error: '請選擇要上傳的圖片' },
+ { status: 400 }
+ );
+ }
+
+ if (!userId) {
+ return NextResponse.json(
+ { success: false, error: '用戶ID不能為空' },
+ { status: 400 }
+ );
+ }
+
+ // 驗證文件類型
+ if (!isValidImageType(file)) {
+ return NextResponse.json(
+ { success: false, error: '只支援 JPEG、PNG、WebP 格式的圖片' },
+ { status: 400 }
+ );
+ }
+
+ // 驗證文件大小(限制 5MB)
+ if (!isValidImageSize(file, 5)) {
+ return NextResponse.json(
+ { success: false, error: '圖片大小不能超過 5MB' },
+ { status: 400 }
+ );
+ }
+
+ // 創建上傳目錄
+ const uploadDir = join(process.cwd(), 'public', 'uploads', 'avatars');
+ await mkdir(uploadDir, { recursive: true });
+
+ // 生成唯一文件名
+ const fileName = generateUniqueFileName(file.name, userId);
+ const filePath = join(uploadDir, fileName);
+
+ // 讀取文件緩衝區
+ const bytes = await file.arrayBuffer();
+ const buffer = Buffer.from(bytes);
+
+ // 使用 Sharp 優化圖片
+ const optimizedBuffer = await sharp(buffer)
+ .resize(200, 200, {
+ fit: 'cover',
+ position: 'center'
+ })
+ .jpeg({ quality: 85 })
+ .toBuffer();
+
+ // 保存優化後的圖片
+ await writeFile(filePath, optimizedBuffer);
+
+ // 生成相對路徑(自動適應不同環境)
+ const imageUrl = `/uploads/avatars/${fileName}`;
+
+ // 更新用戶頭像
+ const userService = new UserService();
+ await userService.update(userId, { avatar: imageUrl });
+
+ console.log(`✅ 用戶 ${userId} 頭像上傳成功: ${imageUrl}`);
+
+ return NextResponse.json({
+ success: true,
+ message: '頭像上傳成功',
+ data: {
+ imageUrl,
+ fileName,
+ fileSize: optimizedBuffer.length,
+ originalSize: file.size
+ }
+ });
+
+ } catch (error) {
+ console.error('頭像上傳失敗:', error);
+ return NextResponse.json(
+ {
+ success: false,
+ error: '頭像上傳失敗',
+ details: error instanceof Error ? error.message : '未知錯誤'
+ },
+ { status: 500 }
+ );
+ }
+}
diff --git a/components/admin/admin-layout.tsx b/components/admin/admin-layout.tsx
index 0454fd5..5319c99 100644
--- a/components/admin/admin-layout.tsx
+++ b/components/admin/admin-layout.tsx
@@ -25,7 +25,7 @@ import {
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
-import { Avatar, AvatarFallback } from "@/components/ui/avatar"
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent } from "@/components/ui/card"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
@@ -312,6 +312,7 @@ export function AdminLayout({ children, currentPage, onPageChange }: AdminLayout
+
{user.name.charAt(0)}
diff --git a/components/admin/user-management.tsx b/components/admin/user-management.tsx
index 7b94ad2..4a6778c 100644
--- a/components/admin/user-management.tsx
+++ b/components/admin/user-management.tsx
@@ -1355,6 +1355,7 @@ export function UserManagement() {
+
{selectedUser.name ? selectedUser.name.charAt(0) : selectedUser.email.charAt(0).toUpperCase()}
diff --git a/components/auth/profile-dialog.tsx b/components/auth/profile-dialog.tsx
index 3fd0107..b97a88e 100644
--- a/components/auth/profile-dialog.tsx
+++ b/components/auth/profile-dialog.tsx
@@ -1,6 +1,6 @@
"use client"
-import { useState } from "react"
+import { useState, useEffect } from "react"
import { useAuth } from "@/contexts/auth-context"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
@@ -31,6 +31,22 @@ export function ProfileDialog({ open, onOpenChange }: ProfileDialogProps) {
phone: user?.phone || "",
location: user?.location || "",
})
+ const [avatar, setAvatar] = useState(user?.avatar || "")
+
+ // 監聽用戶狀態變化,同步更新本地狀態
+ useEffect(() => {
+ if (user) {
+ setProfileData({
+ name: user.name || "",
+ email: user.email || "",
+ department: user.department || "",
+ bio: user.bio || "",
+ phone: user.phone || "",
+ location: user.location || "",
+ })
+ setAvatar(user.avatar || "")
+ }
+ }, [user])
const departments = ["HQBU", "ITBU", "MBU1", "MBU2", "SBU", "財務部", "人資部", "法務部"]
@@ -40,7 +56,7 @@ export function ProfileDialog({ open, onOpenChange }: ProfileDialogProps) {
setIsLoading(true)
try {
- await updateProfile(profileData)
+ await updateProfile({ ...profileData, avatar })
setSuccess("個人資料更新成功!")
setTimeout(() => setSuccess(""), 3000)
} catch (err) {
@@ -50,6 +66,59 @@ export function ProfileDialog({ open, onOpenChange }: ProfileDialogProps) {
}
}
+
+ const handleAvatarFileSelect = async (event: React.ChangeEvent) => {
+ const file = event.target.files?.[0];
+ if (!file) return;
+
+ // 驗證文件類型
+ const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
+ if (!allowedTypes.includes(file.type)) {
+ setError('只支援 JPEG、PNG、WebP 格式的圖片');
+ return;
+ }
+
+ // 驗證文件大小(限制 5MB)
+ const maxSize = 5 * 1024 * 1024; // 5MB
+ if (file.size > maxSize) {
+ setError('圖片大小不能超過 5MB');
+ return;
+ }
+
+ setIsLoading(true);
+ setError('');
+ setSuccess('');
+
+ try {
+ const formData = new FormData();
+ formData.append('avatar', file);
+ formData.append('userId', user?.id || '');
+
+ const response = await fetch('/api/upload/avatar', {
+ method: 'POST',
+ body: formData,
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ setSuccess('頭像上傳成功!');
+ // 更新全局用戶狀態
+ await updateProfile({ avatar: result.data.imageUrl });
+ setTimeout(() => setSuccess(''), 3000);
+ } else {
+ setError(result.error || '上傳失敗');
+ }
+ } catch (error) {
+ const errorMsg = error instanceof Error ? error.message : '上傳失敗';
+ setError(`頭像上傳失敗:${errorMsg}`);
+ } finally {
+ setIsLoading(false);
+ // 清空文件輸入
+ event.target.value = '';
+ }
+ }
+
return (