diff --git a/docs/02_design/AI句子分析規格/AI句子分析功能產品需求規格.md b/docs/01_requirement/AI句子分析功能產品需求規格.md similarity index 100% rename from docs/02_design/AI句子分析規格/AI句子分析功能產品需求規格.md rename to docs/01_requirement/AI句子分析功能產品需求規格.md diff --git a/詞卡管理功能產品需求規格.md b/docs/01_requirement/詞卡管理功能產品需求規格.md similarity index 60% rename from 詞卡管理功能產品需求規格.md rename to docs/01_requirement/詞卡管理功能產品需求規格.md index 5f8c043..a70cf4d 100644 --- a/詞卡管理功能產品需求規格.md +++ b/docs/01_requirement/詞卡管理功能產品需求規格.md @@ -13,11 +13,191 @@ - 📊 學習進度追蹤 - 🔄 批量操作功能 -## 2. 功能需求分析 +## 2. 用戶需求分析 -### 2.1 核心功能模組 +### 2.1 用戶角色定義 -#### 2.1.1 詞卡展示頁面 (FlashcardsPage) +#### 2.1.1 主要用戶 +- **英語學習者**: 使用 DramaLing 學習英語詞彙的用戶 +- **目標**: 有效管理和學習英語詞彙,提升英語水平 +- **技能水平**: 具備基本的電腦操作能力,英語水平從初學者到高級 + +#### 2.1.2 用戶畫像 +- **年齡**: 16-45歲 +- **職業**: 學生、上班族、語言學習愛好者 +- **使用場景**: 通勤時間、休息時間、專門的學習時段 +- **設備**: 手機、平板、電腦 +- **學習目標**: 考試準備、工作需要、興趣提升 + +### 2.2 用戶故事 (User Stories) + +#### 2.2.1 詞卡檢視與瀏覽 +``` +作為一個英語學習者, +我想要瀏覽我所有的詞卡, +這樣我可以回顧我已經學習的詞彙。 + +驗收標準: +- 我可以看到詞卡列表,包含詞彙、翻譯、例句圖片 +- 每個詞卡顯示 CEFR 難度等級和掌握度 +- 我可以在「所有詞卡」和「收藏詞卡」之間切換 +- 詞卡按創建時間排序顯示 +``` + +#### 2.2.2 詞卡搜尋與篩選 +``` +作為一個英語學習者, +我想要快速找到特定的詞卡, +這樣我可以有效地複習特定的詞彙。 + +驗收標準: +- 我可以透過搜尋框輸入關鍵字搜尋詞彙、翻譯或定義 +- 我可以使用進階篩選按 CEFR 等級、詞性、掌握度篩選 +- 我可以使用快速篩選按鈕找到需要加強的詞卡 +- 搜尋結果會高亮顯示關鍵字,並顯示找到的詞卡數量 +``` + +#### 2.2.3 詞卡收藏管理 +``` +作為一個英語學習者, +我想要標記重要的詞卡為收藏, +這樣我可以優先複習重要的詞彙。 + +驗收標準: +- 我可以點擊星星圖標將詞卡加入收藏 +- 已收藏的詞卡會顯示實心黃色星星 +- 我可以在收藏分頁中查看所有收藏的詞卡 +- 我可以隨時取消收藏某個詞卡 +``` + +#### 2.2.4 詞卡編輯與管理 +``` +作為一個英語學習者, +我想要編輯或刪除詞卡, +這樣我可以更正錯誤或移除不需要的詞卡。 + +驗收標準: +- 我可以點擊編輯按鈕修改詞卡的任何欄位 +- 我可以刪除不需要的詞卡,系統會要求確認 +- 編輯後的詞卡會立即更新並保存 +- 刪除操作會顯示成功或失敗的回饋訊息 +``` + +#### 2.2.5 詞卡創建 +``` +作為一個英語學習者, +我想要手動創建新的詞卡, +這樣我可以將遇到的新詞彙加入學習列表。 + +驗收標準: +- 我可以點擊「新增詞卡」按鈕開啟創建表單 +- 我可以填寫詞彙、翻譯、定義、發音、詞性、例句等欄位 +- 提交後新詞卡會出現在詞卡列表中 +- 如果有錯誤,系統會顯示明確的錯誤訊息 +``` + +#### 2.2.6 詞卡詳細檢視 +``` +作為一個英語學習者, +我想要查看詞卡的完整詳細資訊, +這樣我可以深入了解詞彙的各種用法和學習狀態。 + +驗收標準: +- 我可以點擊「詳細」按鈕進入詞卡詳細頁面 +- 詳細頁面顯示所有詞卡信息和學習統計 +- 我可以在詳細頁面進行編輯或收藏操作 +- 我可以查看學習記錄和複習歷史 +``` + +### 2.3 用戶流程 (User Flows) + +#### 2.3.1 詞卡瀏覽流程 +``` +用戶進入詞卡頁面 → 查看詞卡列表 → 選擇分頁(所有詞卡/收藏詞卡) → 瀏覽詞卡 + ↓ +用戶可以執行以下操作: +- 點擊收藏/取消收藏 +- 點擊編輯進入編輯模式 +- 點擊刪除(需確認) +- 點擊詳細查看完整信息 +- 點擊發音按鈕播放音頻 +``` + +#### 2.3.2 詞卡搜尋流程 +``` +用戶進入詞卡頁面 → 點擊搜尋框 → 輸入關鍵字 → 查看即時篩選結果 + ↓ +用戶可以進一步操作: +- 點擊「進階篩選」設定更多條件 +- 使用快速篩選按鈕 +- 清除搜尋條件 +- 對搜尋結果執行詞卡操作 +``` + +#### 2.3.3 詞卡創建流程 +``` +用戶進入詞卡頁面 → 點擊「新增詞卡」按鈕 → 填寫詞卡表單 → 點擊保存 + ↓ +表單驗證: +- 成功:新詞卡出現在列表中,顯示成功訊息 +- 失敗:顯示錯誤訊息,保持表單開啟狀態 +``` + +#### 2.3.4 詞卡編輯流程 +``` +用戶選擇詞卡 → 點擊「編輯」按鈕 → 修改詞卡欄位 → 點擊保存 + ↓ +編輯驗證: +- 成功:詞卡更新,顯示成功訊息,關閉編輯表單 +- 失敗:顯示錯誤訊息,保持編輯模式 +``` + +#### 2.3.5 詞卡刪除流程 +``` +用戶選擇詞卡 → 點擊「刪除」按鈕 → 確認刪除對話框 → 用戶確認 + ↓ +刪除處理: +- 成功:詞卡從列表中移除,顯示成功訊息 +- 失敗:顯示錯誤訊息,詞卡保持在列表中 +``` + +#### 2.3.6 詞卡收藏管理流程 +``` +用戶瀏覽詞卡 → 點擊星星圖標 → 切換收藏狀態 + ↓ +收藏狀態更新: +- 加入收藏:星星變為實心黃色,顯示成功訊息 +- 取消收藏:星星變為空心灰色,顯示成功訊息 +- 在收藏分頁中即時更新詞卡列表 +``` + +#### 2.3.7 AI 生成詞卡流程 (與其他頁面整合) +``` +用戶在詞卡頁面 → 點擊「AI 生成詞卡」按鈕 → 跳轉到 /generate 頁面 + ↓ +AI 分析流程: +用戶輸入句子 → AI 分析 → 查看分析結果 → 點擊保存詞卡 → 返回詞卡頁面查看 +``` + +### 2.4 用戶體驗目標 + +#### 2.4.1 易用性目標 +- **直觀操作**: 新用戶在 5 分鐘內能完成基本詞卡操作 +- **快速搜尋**: 搜尋結果在 300ms 內顯示 +- **清楚回饋**: 所有操作都有明確的成功/失敗回饋 +- **一致設計**: 整個詞卡管理流程保持視覺和交互一致性 + +#### 2.4.2 效率目標 +- **批量操作**: 支援多選和批量操作(未來功能) +- **鍵盤快捷鍵**: 支援 ESC 清除搜尋等常用快捷鍵 +- **智能提示**: 搜尋框提供輸入建議(未來功能) +- **離線功能**: 基本瀏覽功能支援離線使用(未來功能) + +## 3. 功能需求分析 + +### 3.1 核心功能模組 + +#### 3.1.1 詞卡展示頁面 (FlashcardsPage) - **位置**: `/flashcards` - **主要功能**: 集中管理和展示所有詞卡 @@ -281,14 +461,35 @@ class FlashcardsService { - **對比度**: 符合 WCAG 2.1 AA 標準 - **文字大小**: 支援縮放至 200% 而不影響功能 -## 4. 技術實現規格 +## 4. 技術約束與架構 -### 4.1 前端技術架構 -- **框架**: Next.js 15 with App Router -- **語言**: TypeScript -- **樣式**: Tailwind CSS -- **狀態管理**: React useState/useEffect hooks -- **API 通信**: Fetch API with error handling +> 📋 **架構文檔引用** +> +> 本功能需求必須遵循既有的系統架構,完整技術規格請參考: +> - [系統架構總覽](../04_technical/system-architecture.md) +> - [後端架構詳細說明](../04_technical/backend-architecture.md) +> - [前端架構詳細說明](../04_technical/frontend-architecture.md) +> - [詞卡 API 規格](../04_technical/flashcard-api-specification.md) + +### 4.1 技術架構約束 + +#### 前端技術限制 +- **框架**: 必須使用 Next.js 15 with App Router +- **語言**: 必須使用 TypeScript +- **樣式**: 必須使用 Tailwind CSS +- **狀態管理**: 使用 React hooks (useState/useEffect) +- **API 通信**: 使用現有的 flashcardsService + +#### 後端技術限制 +- **框架**: 必須使用 ASP.NET Core 8.0 +- **資料庫**: 必須使用 SQLite + Entity Framework Core +- **API 格式**: 必須遵循現有的 RESTful API 設計 +- **認證**: 必須相容現有的 JWT 認證機制 + +#### 資料模型約束 +- **詞卡模型**: 必須使用現有的 Flashcard.cs 實體 +- **API 回應**: 必須遵循 `{success, data?, error?}` 格式 +- **關聯設計**: 必須保持與 User、CardSet 的現有關聯 ### 4.2 資料處理流程 diff --git a/docs/02_design/AI句子分析規格/DramaLing AI句子分析功能前後端串接實施計劃.md b/docs/03_development/DramaLing AI句子分析功能前後端串接實施計劃.md similarity index 100% rename from docs/02_design/AI句子分析規格/DramaLing AI句子分析功能前後端串接實施計劃.md rename to docs/03_development/DramaLing AI句子分析功能前後端串接實施計劃.md diff --git a/docs/02_design/AI句子分析規格/AI分析API技術實現規格.md b/docs/03_development/api/AI分析API技術實現規格.md similarity index 100% rename from docs/02_design/AI句子分析規格/AI分析API技術實現規格.md rename to docs/03_development/api/AI分析API技術實現規格.md diff --git a/docs/03_development/api/flashcard-management-api.md b/docs/03_development/api/flashcard-management-api.md new file mode 100644 index 0000000..8d3e07e --- /dev/null +++ b/docs/03_development/api/flashcard-management-api.md @@ -0,0 +1,945 @@ +# DramaLing 詞卡管理 API 規格書 + +## 1. API 概覽 + +### 1.1 基本資訊 +- **基礎 URL**: `http://localhost:5008/api` (開發環境) +- **控制器**: `FlashcardsController` +- **路由前綴**: `/api/flashcards` +- **認證方式**: JWT Bearer Token (開發階段暫時關閉) +- **資料格式**: JSON (UTF-8) + +### 1.2 架構依賴 + +> 📋 **技術架構參考文檔** +> +> 本 API 規格書依賴以下文檔,建議閱讀順序: +> +> **🏗️ 系統架構文檔** +> - [系統架構總覽](../../04_technical/system-architecture.md) - 了解整體架構設計 +> - [後端架構詳細說明](../../04_technical/backend-architecture.md) - 了解 ASP.NET Core 架構細節 +> +> **📋 需求規格文檔** +> - [詞卡管理功能產品需求規格](../../01_requirement/詞卡管理功能產品需求規格.md) - 了解功能需求和用戶故事 + +## 2. 資料模型定義 + +### 2.1 詞卡實體 (Flashcard Entity) + +#### C# 實體模型 +```csharp +public class Flashcard +{ + // 基本識別 + public Guid Id { get; set; } + public Guid UserId { get; set; } + public Guid? CardSetId { get; set; } + + // 詞卡內容 + [Required, MaxLength(255)] + public string Word { get; set; } + [Required] + public string Translation { get; set; } + [Required] + public string Definition { get; set; } + [MaxLength(50)] + public string? PartOfSpeech { get; set; } + [MaxLength(255)] + public string? Pronunciation { get; set; } + public string? Example { get; set; } + public string? ExampleTranslation { get; set; } + + // SM-2 學習算法參數 + public float EasinessFactor { get; set; } = 2.5f; + public int Repetitions { get; set; } = 0; + public int IntervalDays { get; set; } = 1; + public DateTime NextReviewDate { get; set; } + + // 學習統計 + [Range(0, 100)] + public int MasteryLevel { get; set; } = 0; + public int TimesReviewed { get; set; } = 0; + public int TimesCorrect { get; set; } = 0; + public DateTime? LastReviewedAt { get; set; } + + // 狀態管理 + public bool IsFavorite { get; set; } = false; + public bool IsArchived { get; set; } = false; + [MaxLength(10)] + public string? DifficultyLevel { get; set; } // A1-C2 + + // 時間戳記 + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} +``` + +#### TypeScript 前端型別定義 +```typescript +interface Flashcard { + id: string; + word: string; + translation: string; + definition: string; + partOfSpeech: string; + pronunciation: string; + example: string; + exampleTranslation?: string; + masteryLevel: number; // 0-100 + timesReviewed: number; + isFavorite: boolean; + nextReviewDate: string; // ISO Date + difficultyLevel: string; // A1, A2, B1, B2, C1, C2 + createdAt: string; // ISO Date + updatedAt?: string; // ISO Date +} + +interface CreateFlashcardRequest { + word: string; + translation: string; + definition: string; + pronunciation: string; + partOfSpeech: string; + example: string; + exampleTranslation?: string; +} +``` + +### 2.2 API 回應格式標準 + +#### 成功回應格式 +```json +{ + "success": true, + "data": { + // 實際資料內容 + }, + "message": "操作成功描述" // 可選 +} +``` + +#### 錯誤回應格式 +```json +{ + "success": false, + "error": "錯誤描述", + "isDuplicate": true, // 特殊情況:重複資料 + "existingCard": { /* 現有詞卡資料 */ } // 重複時的現有資料 +} +``` + +## 3. API 端點規格 + +### 3.1 端點清單 + +| 方法 | 端點 | 描述 | 狀態 | +|------|------|------|------| +| GET | `/api/flashcards` | 取得詞卡列表 | ✅ 已實現 | +| GET | `/api/flashcards/{id}` | 取得單一詞卡 | ✅ 已實現 | +| POST | `/api/flashcards` | 創建新詞卡 | ✅ 已實現 | +| PUT | `/api/flashcards/{id}` | 更新詞卡 | ✅ 已實現 | +| DELETE | `/api/flashcards/{id}` | 刪除詞卡 | ✅ 已實現 | +| POST | `/api/flashcards/{id}/favorite` | 切換收藏狀態 | ✅ 已實現 | + +### 3.2 詳細 API 規格 + +#### 📖 GET /api/flashcards +**功能**: 取得用戶的詞卡列表,支援搜尋和篩選 + +**查詢參數**: +```typescript +interface GetFlashcardsParams { + search?: string; // 搜尋關鍵字,搜尋範圍:詞彙、翻譯、定義 + favoritesOnly?: boolean; // 僅顯示收藏詞卡 (預設: false) +} +``` + +**實際實現邏輯**: +```csharp +// 搜尋篩選邏輯 +if (!string.IsNullOrEmpty(search)) +{ + query = query.Where(f => + f.Word.Contains(search) || + f.Translation.Contains(search) || + (f.Definition != null && f.Definition.Contains(search))); +} + +// 收藏篩選 +if (favoritesOnly) +{ + query = query.Where(f => f.IsFavorite); +} + +// 排序:按創建時間降序 +var flashcards = await query.OrderByDescending(f => f.CreatedAt).ToListAsync(); +``` + +**成功回應**: +```json +{ + "success": true, + "data": { + "flashcards": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "word": "sophisticated", + "translation": "精密的", + "definition": "Highly developed or complex", + "partOfSpeech": "adjective", + "pronunciation": "/səˈfɪstɪkeɪtɪd/", + "example": "A sophisticated system", + "exampleTranslation": "一個精密的系統", + "masteryLevel": 75, + "timesReviewed": 12, + "isFavorite": true, + "nextReviewDate": "2025-09-25T00:00:00Z", + "difficultyLevel": "C1", + "createdAt": "2025-09-20T08:30:00Z", + "updatedAt": "2025-09-24T10:15:00Z" + } + ], + "count": 1 + } +} +``` + +**請求範例**: +```bash +# 取得所有詞卡 +curl "http://localhost:5008/api/flashcards" + +# 搜尋包含 "sophisticated" 的詞卡 +curl "http://localhost:5008/api/flashcards?search=sophisticated" + +# 僅取得收藏詞卡 +curl "http://localhost:5008/api/flashcards?favoritesOnly=true" + +# 組合搜尋:搜尋收藏詞卡中包含 "精密" 的詞卡 +curl "http://localhost:5008/api/flashcards?search=精密&favoritesOnly=true" +``` + +#### 📖 GET /api/flashcards/{id} +**功能**: 取得單一詞卡的完整資訊 + +**路徑參數**: +- `id`: 詞卡唯一識別碼 (GUID 格式) + +**成功回應**: +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "word": "sophisticated", + // ... 完整詞卡資料,格式同列表 API + "createdAt": "2025-09-20T08:30:00Z", + "updatedAt": "2025-09-24T10:15:00Z" + } +} +``` + +**錯誤回應**: +```json +{ + "success": false, + "error": "詞卡不存在" +} +``` + +#### ✏️ POST /api/flashcards +**功能**: 創建新的詞卡 + +**請求體**: +```json +{ + "word": "elaborate", + "translation": "詳細說明", + "definition": "To explain in detail", + "pronunciation": "/ɪˈlæbərət/", + "partOfSpeech": "verb", + "example": "Please elaborate on your idea", + "exampleTranslation": "請詳細說明你的想法" +} +``` + +**實際實現邏輯**: +```csharp +// 1. 自動創建測試用戶 (開發階段) +var testUser = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId); +if (testUser == null) { + // 自動創建測試用戶邏輯 +} + +// 2. 重複詞卡檢測 +var existing = await _context.Flashcards + .FirstOrDefaultAsync(f => f.UserId == userId && + f.Word.ToLower() == request.Word.ToLower() && + !f.IsArchived); + +// 3. 創建新詞卡 +var flashcard = new Flashcard +{ + Id = Guid.NewGuid(), + UserId = userId, + Word = request.Word, + Translation = request.Translation, + // ... 其他欄位 + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow +}; +``` + +**成功回應**: +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "word": "elaborate", + // ... 完整創建的詞卡資料 + "createdAt": "2025-09-24T10:30:00Z" + }, + "message": "詞卡創建成功" +} +``` + +**重複詞卡回應**: +```json +{ + "success": false, + "error": "詞卡已存在", + "isDuplicate": true, + "existingCard": { + "id": "existing-id", + "word": "elaborate", + // ... 現有詞卡資料 + } +} +``` + +#### ✏️ PUT /api/flashcards/{id} +**功能**: 更新現有詞卡 + +**路徑參數**: +- `id`: 詞卡唯一識別碼 (GUID) + +**請求體**: 與 POST 相同格式 + +**實際實現邏輯**: +```csharp +// 1. 查找現有詞卡 +var flashcard = await _context.Flashcards + .FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId && !f.IsArchived); + +if (flashcard == null) +{ + return NotFound(new { Success = false, Error = "詞卡不存在" }); +} + +// 2. 更新欄位 +flashcard.Word = request.Word; +flashcard.Translation = request.Translation; +// ... 更新其他欄位 +flashcard.UpdatedAt = DateTime.UtcNow; + +// 3. 保存變更 +await _context.SaveChangesAsync(); +``` + +**成功回應**: +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + // ... 更新後的完整詞卡資料 + "updatedAt": "2025-09-24T10:35:00Z" + }, + "message": "詞卡更新成功" +} +``` + +#### 🗑️ DELETE /api/flashcards/{id} +**功能**: 刪除詞卡 (軟刪除機制) + +**路徑參數**: +- `id`: 詞卡唯一識別碼 (GUID) + +**實際實現邏輯**: +```csharp +// 軟刪除:設定 IsArchived = true +var flashcard = await _context.Flashcards + .FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId && !f.IsArchived); + +if (flashcard == null) +{ + return NotFound(new { Success = false, Error = "詞卡不存在" }); +} + +flashcard.IsArchived = true; +flashcard.UpdatedAt = DateTime.UtcNow; +await _context.SaveChangesAsync(); +``` + +**成功回應**: +```json +{ + "success": true, + "message": "詞卡已刪除" +} +``` + +#### ⭐ POST /api/flashcards/{id}/favorite +**功能**: 切換詞卡的收藏狀態 + +**路徑參數**: +- `id`: 詞卡唯一識別碼 (GUID) + +**實際實現邏輯**: +```csharp +var flashcard = await _context.Flashcards + .FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId && !f.IsArchived); + +if (flashcard == null) +{ + return NotFound(new { Success = false, Error = "詞卡不存在" }); +} + +// 切換收藏狀態 +flashcard.IsFavorite = !flashcard.IsFavorite; +flashcard.UpdatedAt = DateTime.UtcNow; +await _context.SaveChangesAsync(); +``` + +**成功回應**: +```json +{ + "success": true, + "data": { + "isFavorite": true + }, + "message": "已加入收藏" +} +``` + +## 4. 前端整合規格 + +### 4.1 FlashcardsService 類別 + +#### TypeScript 服務實現 +```typescript +class FlashcardsService { + private readonly baseURL = `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5008'}/api`; + + // 統一請求處理 + private async makeRequest(endpoint: string, options: RequestInit = {}): Promise { + const response = await fetch(`${this.baseURL}${endpoint}`, { + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + ...options, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: 'Network error' })); + throw new Error(errorData.error || `HTTP ${response.status}`); + } + + return response.json(); + } + + // API 方法實現 + async getFlashcards(search?: string, favoritesOnly: boolean = false): Promise> { + const params = new URLSearchParams(); + if (search) params.append('search', search); + if (favoritesOnly) params.append('favoritesOnly', 'true'); + + const queryString = params.toString(); + const endpoint = `/flashcards${queryString ? `?${queryString}` : ''}`; + + return await this.makeRequest>(endpoint); + } + + async createFlashcard(data: CreateFlashcardRequest): Promise> { + return await this.makeRequest>('/flashcards', { + method: 'POST', + body: JSON.stringify(data), + }); + } + + async updateFlashcard(id: string, data: CreateFlashcardRequest): Promise> { + return await this.makeRequest>(`/flashcards/${id}`, { + method: 'PUT', + body: JSON.stringify(data), + }); + } + + async deleteFlashcard(id: string): Promise> { + return await this.makeRequest>(`/flashcards/${id}`, { + method: 'DELETE', + }); + } + + async toggleFavorite(id: string): Promise> { + return await this.makeRequest>(`/flashcards/${id}/favorite`, { + method: 'POST', + }); + } +} + +export const flashcardsService = new FlashcardsService(); +``` + +### 4.2 前端使用範例 + +#### 詞卡列表載入 +```typescript +const loadFlashcards = async () => { + try { + setLoading(true); + const result = await flashcardsService.getFlashcards(); + if (result.success && result.data) { + setFlashcards(result.data.flashcards); + } else { + setError(result.error || 'Failed to load flashcards'); + } + } catch (err) { + setError('Failed to load flashcards'); + } finally { + setLoading(false); + } +}; +``` + +#### 詞卡保存 (含重複檢測) +```typescript +const handleSaveWord = async (word: string, analysis: any) => { + try { + const cardData = { + word: word, + translation: analysis.translation || '', + definition: analysis.definition || '', + pronunciation: analysis.pronunciation || `/${word}/`, + partOfSpeech: analysis.partOfSpeech || 'unknown', + example: `Example sentence with ${word}.` + }; + + const response = await flashcardsService.createFlashcard(cardData); + + if (response.success) { + alert(`✅ 已成功將「${word}」保存到詞卡庫!`); + return { success: true }; + } else if (response.error && response.error.includes('已存在')) { + alert(`⚠️ 詞卡「${word}」已經存在於詞卡庫中`); + return { success: false, error: 'duplicate' }; + } else { + throw new Error(response.error || '保存失敗'); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '保存失敗'; + alert(`❌ 保存詞卡失敗: ${errorMessage}`); + return { success: false, error: errorMessage }; + } +}; +``` + +## 5. 搜尋與篩選功能 + +### 5.1 後端搜尋實現 + +#### 支援的搜尋欄位 +```csharp +// 目前實現的搜尋範圍 +query = query.Where(f => + f.Word.Contains(search) || // 詞彙本身 + f.Translation.Contains(search) || // 中文翻譯 + (f.Definition != null && f.Definition.Contains(search)) // 英文定義 +); + +// 未來可擴展的搜尋範圍 +// f.Example.Contains(search) || // 例句內容 +// f.ExampleTranslation.Contains(search) // 例句翻譯 +``` + +#### 搜尋邏輯特性 +- **大小寫敏感**: 目前使用 `Contains()` 進行大小寫敏感搜尋 +- **部分匹配**: 支援關鍵字部分匹配 +- **邏輯運算**: OR 邏輯 (任一欄位包含關鍵字即匹配) + +### 5.2 前端搜尋與篩選 + +#### 即時搜尋實現 +```typescript +// 前端即時篩選邏輯 +const filteredCards = allCards.filter(card => { + // 基本文字搜尋 + if (searchTerm) { + const searchLower = searchTerm.toLowerCase(); + const matchesText = + card.word?.toLowerCase().includes(searchLower) || + card.translation?.toLowerCase().includes(searchLower) || + card.definition?.toLowerCase().includes(searchLower); + + if (!matchesText) return false; + } + + // CEFR 等級篩選 + if (searchFilters.cefrLevel && card.difficultyLevel !== searchFilters.cefrLevel) { + return false; + } + + // 詞性篩選 + if (searchFilters.partOfSpeech && card.partOfSpeech !== searchFilters.partOfSpeech) { + return false; + } + + // 掌握度篩選 + if (searchFilters.masteryLevel) { + const mastery = card.masteryLevel || 0; + if (searchFilters.masteryLevel === 'high' && mastery < 80) return false; + if (searchFilters.masteryLevel === 'medium' && (mastery < 60 || mastery >= 80)) return false; + if (searchFilters.masteryLevel === 'low' && mastery >= 60) return false; + } + + // 收藏篩選 + if (searchFilters.onlyFavorites && !card.isFavorite) { + return false; + } + + return true; +}); +``` + +#### 進階篩選選項 +```typescript +interface SearchFilters { + cefrLevel: string; // A1, A2, B1, B2, C1, C2 + partOfSpeech: string; // noun, verb, adjective, adverb, preposition, interjection + masteryLevel: string; // high (80%+), medium (60-79%), low (<60%) + onlyFavorites: boolean; // 僅收藏詞卡 +} +``` + +## 6. 錯誤處理機制 + +### 6.1 後端錯誤處理 + +#### 統一錯誤處理模式 +```csharp +try +{ + // API 邏輯 + return Ok(new { Success = true, Data = result }); +} +catch (DbUpdateException ex) +{ + _logger.LogError(ex, "Database error during flashcard operation"); + return StatusCode(500, new { Success = false, Error = "資料庫操作失敗" }); +} +catch (ArgumentException ex) +{ + _logger.LogWarning(ex, "Invalid argument for flashcard operation"); + return BadRequest(new { Success = false, Error = ex.Message }); +} +catch (Exception ex) +{ + _logger.LogError(ex, "Unexpected error during flashcard operation"); + return StatusCode(500, new { Success = false, Error = "內部伺服器錯誤" }); +} +``` + +#### 特殊情況處理 +```csharp +// 重複詞卡檢測 +if (existing != null) +{ + return Ok(new + { + Success = false, + Error = "詞卡已存在", + IsDuplicate = true, + ExistingCard = new { /* 現有詞卡資料 */ } + }); +} + +// 詞卡不存在 +if (flashcard == null) +{ + return NotFound(new { Success = false, Error = "詞卡不存在" }); +} +``` + +### 6.2 前端錯誤處理 + +#### API 服務層錯誤處理 +```typescript +private async makeRequest(endpoint: string, options: RequestInit = {}): Promise { + try { + const response = await fetch(`${this.baseURL}${endpoint}`, options); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: 'Network error' })); + throw new Error(errorData.error || errorData.details || `HTTP ${response.status}`); + } + + return response.json(); + } catch (error) { + console.error('API request failed:', error); + throw error; + } +} +``` + +#### 用戶反饋機制 +```typescript +// 成功操作反饋 +if (response.success) { + alert(`✅ 已成功將「${word}」保存到詞卡庫!`); + return { success: true }; +} + +// 重複詞卡反饋 +else if (response.error && response.error.includes('已存在')) { + alert(`⚠️ 詞卡「${word}」已經存在於詞卡庫中`); + return { success: false, error: 'duplicate' }; +} + +// 一般錯誤反饋 +else { + alert(`❌ 保存詞卡失敗: ${response.error}`); + return { success: false, error: response.error }; +} +``` + +## 7. 認證與授權 + +### 7.1 開發階段認證 + +#### 目前實現 (測試模式) +```csharp +[AllowAnonymous] // 暫時移除認證要求 +public class FlashcardsController : ControllerBase +{ + private Guid GetUserId() + { + // 使用固定測試用戶 ID + return Guid.Parse("00000000-0000-0000-0000-000000000001"); + } +} +``` + +#### 自動測試用戶創建 +```csharp +// 確保測試用戶存在 +var testUser = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId); +if (testUser == null) +{ + testUser = new User + { + Id = userId, + Username = "testuser", + Email = "test@example.com", + DisplayName = "測試用戶", + SubscriptionType = "free", + EnglishLevel = "A2", + CreatedAt = DateTime.UtcNow + }; + _context.Users.Add(testUser); + await _context.SaveChangesAsync(); +} +``` + +### 7.2 生產環境認證 (未來啟用) + +#### JWT Token 解析 +```csharp +[Authorize] // 生產環境啟用 +public class FlashcardsController : ControllerBase +{ + private Guid GetUserId() + { + var userIdString = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? + User.FindFirst("sub")?.Value; + + if (Guid.TryParse(userIdString, out var userId)) + return userId; + + throw new UnauthorizedAccessException("Invalid user ID in token"); + } +} +``` + +## 8. 效能優化 + +### 8.1 資料庫查詢優化 + +#### 索引建議 +```sql +-- 用戶詞卡查詢索引 +CREATE INDEX IX_Flashcards_UserId_IsArchived ON Flashcards(UserId, IsArchived); + +-- 搜尋優化索引 +CREATE INDEX IX_Flashcards_Word ON Flashcards(Word); +CREATE INDEX IX_Flashcards_Translation ON Flashcards(Translation); + +-- 收藏篩選索引 +CREATE INDEX IX_Flashcards_IsFavorite ON Flashcards(IsFavorite); + +-- 複合查詢索引 +CREATE INDEX IX_Flashcards_UserId_IsFavorite_IsArchived ON Flashcards(UserId, IsFavorite, IsArchived); +``` + +#### 查詢優化技巧 +```csharp +// 使用 AsNoTracking 提升查詢效能 (只讀查詢) +var flashcards = await query + .AsNoTracking() + .OrderByDescending(f => f.CreatedAt) + .ToListAsync(); + +// 選擇性載入欄位 (避免載入不必要的關聯資料) +.Select(f => new { + f.Id, f.Word, f.Translation, f.Definition, + // 僅選擇需要的欄位 +}) +``` + +### 8.2 快取策略 (未來實現) + +#### 記憶體快取 +```csharp +// 用戶詞卡列表快取 (30分鐘) +var cacheKey = $"flashcards:user:{userId}"; +var cachedCards = await _cacheService.GetAsync>(cacheKey); + +if (cachedCards == null) +{ + cachedCards = await LoadFlashcardsFromDatabase(userId); + await _cacheService.SetAsync(cacheKey, cachedCards, TimeSpan.FromMinutes(30)); +} +``` + +#### 搜尋結果快取 +```csharp +// 搜尋結果快取 (10分鐘) +var searchCacheKey = $"search:{userId}:{searchTerm}:{favoritesOnly}"; +var cachedResults = await _cacheService.GetAsync(searchCacheKey); +``` + +## 9. 測試規格 + +### 9.1 API 測試用例 + +#### 功能測試 +```bash +# 測試詞卡創建 +curl -X POST http://localhost:5008/api/flashcards \ + -H "Content-Type: application/json" \ + -d '{ + "word": "test", + "translation": "測試", + "definition": "A trial or examination", + "pronunciation": "/test/", + "partOfSpeech": "noun", + "example": "This is a test sentence" + }' + +# 測試搜尋功能 +curl "http://localhost:5008/api/flashcards?search=test" + +# 測試收藏功能 +curl -X POST http://localhost:5008/api/flashcards/{id}/favorite + +# 測試詞卡更新 +curl -X PUT http://localhost:5008/api/flashcards/{id} \ + -H "Content-Type: application/json" \ + -d '{ /* 更新的詞卡資料 */ }' + +# 測試詞卡刪除 +curl -X DELETE http://localhost:5008/api/flashcards/{id} +``` + +#### 邊界條件測試 +```bash +# 測試重複詞卡創建 +curl -X POST http://localhost:5008/api/flashcards \ + -d '{"word": "existing-word", ...}' +# 預期回應: success: false, isDuplicate: true + +# 測試不存在的詞卡操作 +curl http://localhost:5008/api/flashcards/non-existent-id +# 預期回應: 404 Not Found + +# 測試空搜尋 +curl "http://localhost:5008/api/flashcards?search=" +# 預期回應: 返回所有詞卡 +``` + +### 9.2 效能測試 + +#### 載入測試 +```bash +# 測試大量詞卡載入 (1000+ 詞卡) +time curl "http://localhost:5008/api/flashcards" +# 預期: < 2秒 + +# 測試搜尋效能 +time curl "http://localhost:5008/api/flashcards?search=sophisticated" +# 預期: < 300ms +``` + +## 10. 部署與監控 + +### 10.1 健康檢查 + +#### API 健康檢查端點 +```csharp +// Program.cs 中配置 +services.AddHealthChecks() + .AddDbContextCheck(); + +app.MapHealthChecks("/health"); +``` + +**健康檢查請求**: +```bash +curl http://localhost:5008/health +``` + +### 10.2 日誌監控 + +#### 結構化日誌 +```csharp +_logger.LogInformation("Creating flashcard for user {UserId}, word: {Word}", + userId, request.Word); + +_logger.LogError(ex, "Failed to create flashcard for user {UserId}", userId); + +_logger.LogWarning("Duplicate flashcard creation attempt: {Word} for user {UserId}", + request.Word, userId); +``` + +#### 關鍵指標監控 +- **API 響應時間**: 平均 < 200ms +- **成功率**: > 99.5% +- **重複詞卡檢測**: 準確率 100% +- **資料庫連接**: 健康狀態監控 + +--- + +**文檔版本**: v1.0 +**建立日期**: 2025-09-24 +**基於**: FlashcardsController.cs v1.0 +**維護負責**: API 開發團隊 +**更新頻率**: 控制器變更時同步更新 + +> 📋 **相關參考文檔** +> +> **📋 需求與規格** +> - [詞卡管理功能需求規格](../../01_requirement/詞卡管理功能產品需求規格.md) - 查看完整功能需求和用戶故事 +> +> **🏗️ 技術架構** +> - [後端架構詳細說明](../../04_technical/backend-architecture.md) - 了解後端技術實現細節 +> - [前端架構詳細說明](../../04_technical/frontend-architecture.md) - 了解前端整合方式 \ No newline at end of file diff --git a/docs/04_technical/backend-architecture.md b/docs/04_technical/backend-architecture.md new file mode 100644 index 0000000..8b0641a --- /dev/null +++ b/docs/04_technical/backend-architecture.md @@ -0,0 +1,590 @@ +# DramaLing 後端架構詳細說明 + +## 1. 技術棧概覽 + +### 1.1 核心技術 +- **框架**: ASP.NET Core 8.0 +- **語言**: C# .NET 8 +- **ORM**: Entity Framework Core 8.0 +- **資料庫**: SQLite 3.x +- **認證**: JWT Bearer Token +- **依賴注入**: Microsoft.Extensions.DependencyInjection + +### 1.2 專案結構 + +``` +backend/DramaLing.Api/ +├── Controllers/ # API 控制器 +│ ├── FlashcardsController.cs +│ ├── AIController.cs +│ └── AuthController.cs +├── Models/ +│ ├── Entities/ # 資料模型 +│ │ ├── Flashcard.cs +│ │ ├── User.cs +│ │ └── CardSet.cs +│ ├── DTOs/ # 資料傳輸物件 +│ └── Configuration/ # 配置模型 +├── Data/ # 資料存取層 +│ ├── DramaLingDbContext.cs +│ └── Migrations/ +├── Services/ # 業務邏輯層 +│ ├── AI/ # AI 服務 +│ ├── Caching/ # 快取服務 +│ └── AuthService.cs +├── Extensions/ # 擴展方法 +│ └── ServiceCollectionExtensions.cs +└── Program.cs # 應用程式入口 +``` + +## 2. 資料模型架構 + +### 2.1 詞卡實體模型 (Flashcard) + +```csharp +public class Flashcard +{ + // 主鍵和關聯 + public Guid Id { get; set; } + public Guid UserId { get; set; } + public Guid? CardSetId { get; set; } + + // 詞卡內容 + [Required, MaxLength(255)] + public string Word { get; set; } + [Required] + public string Translation { get; set; } + [Required] + public string Definition { get; set; } + [MaxLength(50)] + public string? PartOfSpeech { get; set; } + [MaxLength(255)] + public string? Pronunciation { get; set; } + public string? Example { get; set; } + public string? ExampleTranslation { get; set; } + + // SM-2 學習算法參數 + public float EasinessFactor { get; set; } = 2.5f; + public int Repetitions { get; set; } = 0; + public int IntervalDays { get; set; } = 1; + public DateTime NextReviewDate { get; set; } + + // 學習統計 + [Range(0, 100)] + public int MasteryLevel { get; set; } = 0; + public int TimesReviewed { get; set; } = 0; + public int TimesCorrect { get; set; } = 0; + public DateTime? LastReviewedAt { get; set; } + + // 狀態管理 + public bool IsFavorite { get; set; } = false; + public bool IsArchived { get; set; } = false; + [MaxLength(10)] + public string? DifficultyLevel { get; set; } // A1-C2 + + // 時間戳記 + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + + // 導航屬性 + public virtual User User { get; set; } + public virtual CardSet? CardSet { get; set; } + public virtual ICollection StudyRecords { get; set; } + public virtual ICollection FlashcardTags { get; set; } + public virtual ICollection ErrorReports { get; set; } +} +``` + +### 2.2 資料庫關聯設計 + +``` +Users (1) ──────────────── (*) Flashcards + │ │ + │ │ (*) + │ │ + └─── (1) CardSets (*) ───────┘ + +StudyRecords (*) ──── (1) Flashcards +ErrorReports (*) ──── (1) Flashcards +FlashcardTags (*) ─── (1) Flashcards +``` + +## 3. API 架構設計 + +### 3.1 控制器架構 + +#### FlashcardsController.cs +```csharp +[ApiController] +[Route("api/flashcards")] +[AllowAnonymous] // 開發階段暫時移除認證 +public class FlashcardsController : ControllerBase +{ + private readonly DramaLingDbContext _context; + private readonly ILogger _logger; + + // 標準 RESTful API 端點 + [HttpGet] // GET /api/flashcards + [HttpGet("{id}")] // GET /api/flashcards/{id} + [HttpPost] // POST /api/flashcards + [HttpPut("{id}")] // PUT /api/flashcards/{id} + [HttpDelete("{id}")] // DELETE /api/flashcards/{id} + [HttpPost("{id}/favorite")] // POST /api/flashcards/{id}/favorite +} +``` + +### 3.2 API 回應格式標準化 + +#### 成功回應格式 +```json +{ + "success": true, + "data": { + "flashcards": [...], + "count": 42 + }, + "message": "操作成功" +} +``` + +#### 錯誤回應格式 +```json +{ + "success": false, + "error": "錯誤描述", + "details": "詳細錯誤信息", + "timestamp": "2025-09-24T10:30:00Z" +} +``` + +### 3.3 查詢參數支援 + +#### GET /api/flashcards +```csharp +public async Task GetFlashcards( + [FromQuery] string? search = null, // 搜尋關鍵字 + [FromQuery] bool favoritesOnly = false // 僅收藏詞卡 +) +``` + +## 4. 服務層架構 + +### 4.1 依賴注入配置 (ServiceCollectionExtensions.cs) + +```csharp +public static class ServiceCollectionExtensions +{ + // 資料庫服務配置 + public static IServiceCollection AddDatabaseServices(...) + + // Repository 服務配置 + public static IServiceCollection AddRepositoryServices(...) + + // 快取服務配置 + public static IServiceCollection AddCachingServices(...) + + // AI 服務配置 + public static IServiceCollection AddAIServices(...) + + // 業務服務配置 + public static IServiceCollection AddBusinessServices(...) + + // 認證服務配置 + public static IServiceCollection AddAuthenticationServices(...) + + // CORS 政策配置 + public static IServiceCollection AddCorsServices(...) +} +``` + +### 4.2 業務服務層 + +#### 已實現的服務 +```csharp +// 認證服務 +services.AddScoped(); + +// 使用量追蹤 +services.AddScoped(); + +// Azure 語音服務 +services.AddScoped(); + +// 音頻快取 +services.AddScoped(); + +// AI 提供商管理 +services.AddScoped(); +services.AddScoped(); +``` + +## 5. 資料存取層 + +### 5.1 DbContext 配置 + +```csharp +public class DramaLingDbContext : DbContext +{ + public DbSet Users { get; set; } + public DbSet Flashcards { get; set; } + public DbSet CardSets { get; set; } + public DbSet StudyRecords { get; set; } + public DbSet ErrorReports { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // 詞卡配置 + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Word).IsRequired().HasMaxLength(255); + entity.Property(e => e.Translation).IsRequired(); + entity.Property(e => e.Definition).IsRequired(); + + // 關聯配置 + entity.HasOne(f => f.User) + .WithMany(u => u.Flashcards) + .HasForeignKey(f => f.UserId); + + entity.HasOne(f => f.CardSet) + .WithMany(cs => cs.Flashcards) + .HasForeignKey(f => f.CardSetId) + .IsRequired(false); // CardSetId 可為空 + }); + } +} +``` + +### 5.2 資料庫連接配置 + +#### 開發環境 +```csharp +// 環境變數或配置檔案 +var connectionString = Environment.GetEnvironmentVariable("DRAMALING_DB_CONNECTION") + ?? configuration.GetConnectionString("DefaultConnection") + ?? "Data Source=dramaling_test.db"; + +services.AddDbContext(options => + options.UseSqlite(connectionString)); +``` + +#### 記憶體資料庫 (測試用) +```csharp +var useInMemoryDb = Environment.GetEnvironmentVariable("USE_INMEMORY_DB") == "true"; +if (useInMemoryDb) +{ + services.AddDbContext(options => + options.UseSqlite("Data Source=:memory:")); +} +``` + +## 6. 認證與授權 + +### 6.1 JWT 配置 + +```csharp +public static IServiceCollection AddAuthenticationServices(...) +{ + var supabaseUrl = Environment.GetEnvironmentVariable("DRAMALING_SUPABASE_URL") + ?? "https://localhost"; + var jwtSecret = Environment.GetEnvironmentVariable("DRAMALING_SUPABASE_JWT_SECRET") + ?? "dev-secret-minimum-32-characters-long-for-jwt-signing-in-development-mode-only"; + + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = supabaseUrl, + ValidAudience = "authenticated", + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)) + }; + }); +} +``` + +### 6.2 開發階段認證處理 + +```csharp +// 暫時移除認證要求,使用固定測試用戶 +private Guid GetUserId() +{ + return Guid.Parse("00000000-0000-0000-0000-000000000001"); + + // 生產環境將啟用: + // var userIdString = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + // if (Guid.TryParse(userIdString, out var userId)) + // return userId; + // throw new UnauthorizedAccessException("Invalid user ID in token"); +} +``` + +## 7. CORS 設定 + +### 7.1 跨域政策配置 + +```csharp +services.AddCors(options => +{ + options.AddPolicy("AllowFrontend", policy => + { + policy.WithOrigins("http://localhost:3000", "http://localhost:3001", "http://localhost:3002") + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials() + .SetPreflightMaxAge(TimeSpan.FromMinutes(5)); + }); + + options.AddPolicy("AllowAll", policy => + { + policy.AllowAnyOrigin() + .AllowAnyHeader() + .AllowAnyMethod(); + }); +}); +``` + +## 8. AI 服務整合 + +### 8.1 AI 提供商架構 + +```csharp +// AI 提供商介面 +public interface IAIProvider +{ + Task AnalyzeSentenceAsync(string inputText, AnalysisOptions options); +} + +// Gemini AI 實作 +public class GeminiAIProvider : IAIProvider +{ + private readonly HttpClient _httpClient; + private readonly GeminiOptions _options; + + public async Task AnalyzeSentenceAsync(...) + { + // 調用 Google Gemini API + // 處理回應和錯誤 + // 返回標準化結果 + } +} +``` + +### 8.2 AI 服務配置 + +```csharp +// 強型別配置 +services.Configure(configuration.GetSection(GeminiOptions.SectionName)); +services.AddSingleton, GeminiOptionsValidator>(); + +// AI 服務註冊 +services.AddHttpClient(); +services.AddScoped(); +services.AddScoped(); +``` + +## 9. 錯誤處理架構 + +### 9.1 全域異常處理 + +```csharp +app.UseExceptionHandler(errorApp => +{ + errorApp.Run(async context => + { + var errorFeature = context.Features.Get(); + if (errorFeature != null) + { + var logger = context.RequestServices.GetRequiredService>(); + logger.LogError(errorFeature.Error, "Unhandled exception occurred"); + + context.Response.StatusCode = 500; + context.Response.ContentType = "application/json"; + + var response = new + { + success = false, + error = "Internal server error", + timestamp = DateTime.UtcNow + }; + + await context.Response.WriteAsync(JsonSerializer.Serialize(response)); + } + }); +}); +``` + +### 9.2 控制器級錯誤處理 + +```csharp +try +{ + var result = await flashcardsService.CreateFlashcard(data); + return Ok(new { success = true, data = result }); +} +catch (ValidationException ex) +{ + return BadRequest(new { success = false, error = ex.Message }); +} +catch (DbUpdateException ex) +{ + _logger.LogError(ex, "Database error during flashcard creation"); + return StatusCode(500, new { success = false, error = "Database operation failed" }); +} +catch (Exception ex) +{ + _logger.LogError(ex, "Unexpected error during flashcard creation"); + return StatusCode(500, new { success = false, error = "Internal server error" }); +} +``` + +## 10. 開發與部署 + +### 10.1 開發環境設定 + +#### 啟動開發伺服器 +```bash +cd backend +dotnet run --project DramaLing.Api + +# 伺服器運行於: http://localhost:5008 +# Swagger UI: http://localhost:5008/swagger +``` + +#### 環境變數設定 +```bash +export DRAMALING_DB_CONNECTION="Data Source=dramaling_test.db" +export DRAMALING_SUPABASE_URL="https://localhost" +export DRAMALING_SUPABASE_JWT_SECRET="dev-secret-minimum-32-characters-long-for-jwt-signing-in-development-mode-only" +export USE_INMEMORY_DB="false" +``` + +### 10.2 資料庫管理 + +#### Entity Framework 遷移 +```bash +# 新增遷移 +dotnet ef migrations add MigrationName + +# 更新資料庫 +dotnet ef database update + +# 查看遷移狀態 +dotnet ef migrations list +``` + +#### 測試資料初始化 +```csharp +// 自動創建測試用戶 +var testUser = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId); +if (testUser == null) +{ + testUser = new User + { + Id = userId, + Email = "test@dramaling.com", + Name = "Test User", + CreatedAt = DateTime.UtcNow + }; + _context.Users.Add(testUser); + await _context.SaveChangesAsync(); +} +``` + +## 11. 效能優化 + +### 11.1 查詢優化 +```csharp +// 使用 AsNoTracking 提升查詢效能 +var flashcards = await _context.Flashcards + .AsNoTracking() + .Where(f => f.UserId == userId) + .OrderByDescending(f => f.CreatedAt) + .ToListAsync(); + +// 避免 N+1 查詢問題 +var flashcardsWithDetails = await _context.Flashcards + .Include(f => f.StudyRecords) + .Include(f => f.CardSet) + .Where(f => f.UserId == userId) + .ToListAsync(); +``` + +### 11.2 快取策略 +```csharp +// 記憶體快取服務 +services.AddMemoryCache(); +services.AddScoped(); + +// 快取使用範例 +var cacheKey = $"flashcards:user:{userId}"; +var cachedCards = await _cacheService.GetAsync>(cacheKey); +if (cachedCards == null) +{ + cachedCards = await LoadFlashcardsFromDatabase(userId); + await _cacheService.SetAsync(cacheKey, cachedCards, TimeSpan.FromMinutes(30)); +} +``` + +## 12. 安全性措施 + +### 12.1 輸入驗證 +```csharp +// 模型驗證特性 +[Required, MaxLength(255)] +public string Word { get; set; } + +// 控制器層驗證 +if (!ModelState.IsValid) +{ + return BadRequest(ModelState); +} +``` + +### 12.2 SQL 注入防護 +```csharp +// Entity Framework 自動參數化查詢 +var flashcards = _context.Flashcards + .Where(f => f.Word.Contains(searchTerm)) // 自動參數化 + .ToList(); +``` + +### 12.3 XSS 防護 +```csharp +// 自動 HTML 編碼 +public string Definition { get; set; } // EF Core 自動處理 +``` + +## 13. 監控與日誌 + +### 13.1 結構化日誌 +```csharp +_logger.LogInformation("Creating flashcard for user {UserId}, word: {Word}", + userId, request.Word); + +_logger.LogError(ex, "Failed to create flashcard for user {UserId}", userId); +``` + +### 13.2 健康檢查 +```csharp +services.AddHealthChecks() + .AddDbContextCheck(); + +app.MapHealthChecks("/health"); +``` + +--- + +**文檔版本**: v1.0 +**建立日期**: 2025-09-24 +**維護負責**: 後端開發團隊 +**下次審核**: 架構變更時 + +> 📋 相關文檔: +> - [系統架構總覽](./system-architecture.md) +> - [前端架構詳細說明](./frontend-architecture.md) +> - [詞卡 API 規格](./flashcard-api-specification.md) \ No newline at end of file diff --git a/docs/04_technical/flashcard-api-specification.md b/docs/04_technical/flashcard-api-specification.md new file mode 100644 index 0000000..16bef82 --- /dev/null +++ b/docs/04_technical/flashcard-api-specification.md @@ -0,0 +1,579 @@ +# DramaLing API 規格總覽 + +## 1. API 架構概覽 + +### 1.1 基礎資訊 +- **基礎 URL**: `http://localhost:5008/api` (開發環境) +- **協議**: HTTP/HTTPS +- **資料格式**: JSON +- **認證方式**: JWT Bearer Token +- **CORS**: 允許 localhost:3000-3002 + +### 1.2 標準回應格式 + +#### 成功回應格式 +```json +{ + "success": true, + "data": { + // 實際資料內容 + }, + "message": "操作成功" // 可選 +} +``` + +#### 錯誤回應格式 +```json +{ + "success": false, + "error": "錯誤描述", + "details": "詳細錯誤信息", // 可選 + "timestamp": "2025-09-24T10:30:00Z" // 可選 +} +``` + +## 2. 詞卡管理 API + +### 2.1 API 端點清單 + +| 方法 | 端點 | 描述 | 認證 | +|------|------|------|------| +| GET | `/api/flashcards` | 取得詞卡列表 | ✅ | +| GET | `/api/flashcards/{id}` | 取得單一詞卡 | ✅ | +| POST | `/api/flashcards` | 創建新詞卡 | ✅ | +| PUT | `/api/flashcards/{id}` | 更新詞卡 | ✅ | +| DELETE | `/api/flashcards/{id}` | 刪除詞卡 | ✅ | +| POST | `/api/flashcards/{id}/favorite` | 切換收藏狀態 | ✅ | + +### 2.2 詳細 API 規格 + +#### GET /api/flashcards +**描述**: 取得用戶的詞卡列表,支援搜尋和篩選 + +**查詢參數**: +```typescript +interface GetFlashcardsQuery { + search?: string; // 搜尋關鍵字 (詞彙、翻譯、定義) + favoritesOnly?: boolean; // 僅顯示收藏詞卡 (預設: false) +} +``` + +**回應範例**: +```json +{ + "success": true, + "data": { + "flashcards": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "word": "sophisticated", + "translation": "精密的", + "definition": "Highly developed or complex", + "partOfSpeech": "adjective", + "pronunciation": "/səˈfɪstɪkeɪtɪd/", + "example": "A sophisticated system", + "exampleTranslation": "一個精密的系統", + "masteryLevel": 75, + "timesReviewed": 12, + "isFavorite": true, + "nextReviewDate": "2025-09-25T00:00:00Z", + "difficultyLevel": "C1", + "createdAt": "2025-09-20T08:30:00Z", + "updatedAt": "2025-09-24T10:15:00Z" + } + ], + "count": 1 + } +} +``` + +#### POST /api/flashcards +**描述**: 創建新的詞卡 + +**請求體**: +```json +{ + "word": "sophisticated", + "translation": "精密的", + "definition": "Highly developed or complex", + "pronunciation": "/səˈfɪstɪkeɪtɪd/", + "partOfSpeech": "adjective", + "example": "A sophisticated system", + "exampleTranslation": "一個精密的系統" +} +``` + +**回應範例**: +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "word": "sophisticated", + // ... 完整詞卡資料 + "createdAt": "2025-09-24T10:30:00Z" + }, + "message": "詞卡創建成功" +} +``` + +#### PUT /api/flashcards/{id} +**描述**: 更新現有詞卡 + +**路徑參數**: +- `id`: 詞卡唯一識別碼 (GUID) + +**請求體**: 與 POST 相同格式 + +**回應範例**: +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + // ... 更新後的詞卡資料 + "updatedAt": "2025-09-24T10:35:00Z" + }, + "message": "詞卡更新成功" +} +``` + +#### DELETE /api/flashcards/{id} +**描述**: 刪除詞卡 (軟刪除,設定 IsArchived = true) + +**路徑參數**: +- `id`: 詞卡唯一識別碼 (GUID) + +**回應範例**: +```json +{ + "success": true, + "message": "詞卡已刪除" +} +``` + +#### POST /api/flashcards/{id}/favorite +**描述**: 切換詞卡的收藏狀態 + +**路徑參數**: +- `id`: 詞卡唯一識別碼 (GUID) + +**回應範例**: +```json +{ + "success": true, + "data": { + "isFavorite": true + }, + "message": "已加入收藏" +} +``` + +## 3. AI 分析 API + +### 3.1 API 端點 + +| 方法 | 端點 | 描述 | 認證 | +|------|------|------|------| +| POST | `/api/ai/analyze-sentence` | AI 句子分析 | ✅ | + +### 3.2 句子分析 API + +#### POST /api/ai/analyze-sentence +**描述**: 使用 AI 分析英語句子,提供詞彙分析、語法檢查、翻譯等功能 + +**請求體**: +```json +{ + "inputText": "The sophisticated algorithm processes data efficiently.", + "analysisMode": "full", + "options": { + "includeGrammarCheck": true, + "includeVocabularyAnalysis": true, + "includeTranslation": true, + "includeIdiomDetection": true, + "includeExamples": true + } +} +``` + +**回應範例**: +```json +{ + "success": true, + "data": { + "originalText": "The sophisticated algorithm processes data efficiently.", + "sentenceMeaning": "這個精密的算法高效地處理資料。", + "grammarCorrection": { + "hasErrors": false, + "correctedText": null, + "corrections": [], + "confidenceScore": 0.95 + }, + "vocabularyAnalysis": { + "sophisticated": { + "word": "sophisticated", + "translation": "精密的", + "definition": "Highly developed or complex", + "partOfSpeech": "adjective", + "pronunciation": "/səˈfɪstɪkeɪtɪd/", + "difficultyLevel": "C1", + "frequency": "high", + "cefrLevel": "C1", + "synonyms": ["advanced", "complex", "refined"] + }, + "algorithm": { + "word": "algorithm", + "translation": "算法", + "definition": "A set of rules for solving problems", + "partOfSpeech": "noun", + "pronunciation": "/ˈælɡərɪðəm/", + "difficultyLevel": "B2", + "frequency": "medium", + "cefrLevel": "B2" + } + }, + "idioms": [ + { + "idiom": "processes data", + "translation": "處理資料", + "definition": "To handle and analyze information", + "difficultyLevel": "B1", + "frequency": "high", + "cefrLevel": "B1" + } + ] + }, + "processingTime": "2.34s" +} +``` + +## 4. 認證 API + +### 4.1 API 端點 + +| 方法 | 端點 | 描述 | 認證 | +|------|------|------|------| +| POST | `/api/auth/login` | 用戶登入 | ❌ | +| POST | `/api/auth/register` | 用戶註冊 | ❌ | +| POST | `/api/auth/refresh` | 更新 Token | ✅ | +| POST | `/api/auth/logout` | 用戶登出 | ✅ | + +### 4.2 認證流程 + +#### 開發階段認證 +```csharp +// 目前使用固定測試用戶 ID +private Guid GetUserId() +{ + return Guid.Parse("00000000-0000-0000-0000-000000000001"); +} + +// 控制器暫時設定為 [AllowAnonymous] +[AllowAnonymous] +public class FlashcardsController : ControllerBase +``` + +#### 生產環境認證 (未來啟用) +```csharp +// JWT Token 解析 +private Guid GetUserId() +{ + var userIdString = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (Guid.TryParse(userIdString, out var userId)) + return userId; + throw new UnauthorizedAccessException("Invalid user ID in token"); +} +``` + +## 5. 錯誤代碼標準 + +### 5.1 HTTP 狀態碼使用 + +| 狀態碼 | 意義 | 使用場景 | +|--------|------|----------| +| 200 | OK | 請求成功 | +| 201 | Created | 資源創建成功 | +| 400 | Bad Request | 請求參數錯誤 | +| 401 | Unauthorized | 認證失敗 | +| 403 | Forbidden | 權限不足 | +| 404 | Not Found | 資源不存在 | +| 409 | Conflict | 資源衝突 (如重複創建) | +| 500 | Internal Server Error | 伺服器內部錯誤 | + +### 5.2 自定義錯誤碼 + +| 錯誤碼 | 描述 | HTTP 狀態 | +|--------|------|-----------| +| `FLASHCARD_NOT_FOUND` | 詞卡不存在 | 404 | +| `FLASHCARD_ALREADY_EXISTS` | 詞卡已存在 | 409 | +| `INVALID_CEFR_LEVEL` | 無效的 CEFR 等級 | 400 | +| `USER_NOT_FOUND` | 用戶不存在 | 404 | +| `DATABASE_ERROR` | 資料庫操作失敗 | 500 | +| `AI_SERVICE_UNAVAILABLE` | AI 服務不可用 | 503 | + +## 6. 請求/回應範例 + +### 6.1 詞卡 CRUD 完整範例 + +#### 創建詞卡 +```bash +curl -X POST http://localhost:5008/api/flashcards \ + -H "Content-Type: application/json" \ + -d '{ + "word": "elaborate", + "translation": "詳細說明", + "definition": "To explain in detail", + "pronunciation": "/ɪˈlæbərət/", + "partOfSpeech": "verb", + "example": "Please elaborate on your idea", + "exampleTranslation": "請詳細說明你的想法" + }' +``` + +#### 更新詞卡 +```bash +curl -X PUT http://localhost:5008/api/flashcards/550e8400-e29b-41d4-a716-446655440000 \ + -H "Content-Type: application/json" \ + -d '{ + "word": "elaborate", + "translation": "詳細闡述", + "definition": "To explain something in greater detail", + "pronunciation": "/ɪˈlæbərət/", + "partOfSpeech": "verb", + "example": "Could you elaborate on that point?", + "exampleTranslation": "你能詳細闡述那個觀點嗎?" + }' +``` + +#### 查詢詞卡 (帶搜尋) +```bash +curl "http://localhost:5008/api/flashcards?search=elaborate&favoritesOnly=false" +``` + +#### 切換收藏 +```bash +curl -X POST http://localhost:5008/api/flashcards/550e8400-e29b-41d4-a716-446655440000/favorite +``` + +### 6.2 AI 分析範例 + +#### 句子分析請求 +```bash +curl -X POST http://localhost:5008/api/ai/analyze-sentence \ + -H "Content-Type: application/json" \ + -d '{ + "inputText": "I need to elaborate on this concept", + "analysisMode": "full", + "options": { + "includeGrammarCheck": true, + "includeVocabularyAnalysis": true, + "includeTranslation": true, + "includeIdiomDetection": true, + "includeExamples": true + } + }' +``` + +## 7. 資料驗證規則 + +### 7.1 詞卡資料驗證 + +#### 必填欄位 +```csharp +[Required] +[MaxLength(255)] +public string Word { get; set; } + +[Required] +public string Translation { get; set; } + +[Required] +public string Definition { get; set; } + +[Required] +public string Example { get; set; } +``` + +#### 選填欄位約束 +```csharp +[MaxLength(50)] +public string? PartOfSpeech { get; set; } + +[MaxLength(255)] +public string? Pronunciation { get; set; } + +[MaxLength(10)] +public string? DifficultyLevel { get; set; } // A1, A2, B1, B2, C1, C2 +``` + +#### 數值範圍驗證 +```csharp +[Range(0, 100)] +public int MasteryLevel { get; set; } = 0; + +[Range(0, int.MaxValue)] +public int TimesReviewed { get; set; } = 0; +``` + +### 7.2 前端驗證規則 + +#### TypeScript 型別約束 +```typescript +interface CreateFlashcardRequest { + word: string; // 1-255 字元 + translation: string; // 必填 + definition: string; // 必填 + pronunciation: string; // 選填,建議 IPA 格式 + partOfSpeech: string; // 選填,預設 'noun' + example: string; // 必填 + exampleTranslation?: string; // 選填 +} +``` + +## 8. 快取策略 + +### 8.1 伺服器端快取 + +#### 記憶體快取 +```csharp +// 常用詞卡快取 30 分鐘 +var cacheKey = $"flashcards:user:{userId}"; +await _cacheService.SetAsync(cacheKey, flashcards, TimeSpan.FromMinutes(30)); +``` + +#### 查詢結果快取 +```csharp +// 搜尋結果快取 10 分鐘 +var searchCacheKey = $"search:{userId}:{searchTerm}:{favoritesOnly}"; +await _cacheService.SetAsync(searchCacheKey, results, TimeSpan.FromMinutes(10)); +``` + +### 8.2 客戶端快取 + +#### API 服務層快取 +```typescript +// 簡單的記憶體快取 (未來可改用 SWR 或 React Query) +class FlashcardsService { + private cache = new Map(); + + async getFlashcards(search?: string, favoritesOnly: boolean = false) { + const cacheKey = `flashcards:${search || 'all'}:${favoritesOnly}`; + + if (this.cache.has(cacheKey)) { + return this.cache.get(cacheKey); + } + + const result = await this.makeRequest(endpoint); + this.cache.set(cacheKey, result); + + // 5 分鐘後清除快取 + setTimeout(() => this.cache.delete(cacheKey), 5 * 60 * 1000); + + return result; + } +} +``` + +## 9. 速率限制 + +### 9.1 API 速率限制 (未來實作) + +| 端點類型 | 限制 | 時間窗口 | +|----------|------|----------| +| 詞卡 CRUD | 100 requests | 每分鐘 | +| AI 分析 | 10 requests | 每分鐘 | +| 搜尋 | 200 requests | 每分鐘 | + +### 9.2 使用量追蹤 + +#### AI API 使用量 +```csharp +// 記錄 AI API 使用量 +public class UsageTrackingService +{ + public async Task RecordApiUsage(Guid userId, string apiType, decimal cost) + { + var usage = new ApiUsage + { + UserId = userId, + ApiType = apiType, + Cost = cost, + Timestamp = DateTime.UtcNow + }; + + _context.ApiUsages.Add(usage); + await _context.SaveChangesAsync(); + } +} +``` + +## 10. 開發工具 + +### 10.1 API 文檔 + +#### Swagger 配置 +```csharp +services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new() { Title = "DramaLing API", Version = "v1" }); + + // JWT 認證配置 + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "JWT Authorization header using the Bearer scheme", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer" + }); +}); + +// 存取位置: http://localhost:5008/swagger +``` + +### 10.2 API 測試 + +#### 使用 curl 測試 +```bash +# 設定基礎 URL +export API_BASE="http://localhost:5008/api" + +# 測試詞卡列表 +curl "$API_BASE/flashcards" + +# 測試詞卡創建 +curl -X POST "$API_BASE/flashcards" \ + -H "Content-Type: application/json" \ + -d @test-flashcard.json +``` + +#### 使用 Postman Collection (未來) +```json +{ + "info": { + "name": "DramaLing API", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Flashcards", + "item": [ + // 詞卡相關 API 測試 + ] + } + ] +} +``` + +--- + +**文檔版本**: v1.0 +**建立日期**: 2025-09-24 +**維護負責**: API 開發團隊 +**更新頻率**: API 變更時即時更新 + +> 📋 相關文檔: +> - [系統架構總覽](./system-architecture.md) +> - [後端架構詳細說明](./backend-architecture.md) +> - [前端架構詳細說明](./frontend-architecture.md) \ No newline at end of file diff --git a/docs/04_technical/frontend-architecture.md b/docs/04_technical/frontend-architecture.md new file mode 100644 index 0000000..fc1ea55 --- /dev/null +++ b/docs/04_technical/frontend-architecture.md @@ -0,0 +1,693 @@ +# DramaLing 前端架構詳細說明 + +## 1. 技術棧概覽 + +### 1.1 核心技術 +- **框架**: Next.js 15 (App Router) +- **語言**: TypeScript 5.x +- **樣式框架**: Tailwind CSS 3.x +- **UI 組件**: React 18.x + 自定義組件 +- **狀態管理**: React useState/useEffect hooks +- **API 通信**: Fetch API + 自定義 Service 層 + +### 1.2 開發工具 +- **套件管理**: npm +- **建置工具**: Next.js 內建 (Webpack + SWC) +- **型別檢查**: TypeScript Compiler +- **代碼格式化**: Prettier (如果配置) +- **代碼檢查**: ESLint (如果配置) + +## 2. 專案結構 + +### 2.1 目錄架構 + +``` +frontend/ +├── app/ # Next.js App Router 頁面 +│ ├── flashcards/ # 詞卡管理頁面 +│ │ ├── page.tsx # 詞卡列表頁面 +│ │ └── [id]/ # 動態詞卡詳細頁面 +│ │ └── page.tsx +│ ├── generate/ # AI 生成詞卡頁面 +│ │ └── page.tsx +│ ├── settings/ # 設定頁面 +│ │ └── page.tsx +│ ├── layout.tsx # 根佈局 +│ ├── page.tsx # 首頁 +│ └── globals.css # 全域樣式 +├── components/ # 可重用組件 +│ ├── ClickableTextV2.tsx # 可點擊文字組件 +│ ├── FlashcardForm.tsx # 詞卡表單組件 +│ ├── Navigation.tsx # 導航組件 +│ ├── ProtectedRoute.tsx # 路由保護組件 +│ └── AudioPlayer.tsx # 音頻播放組件 +├── lib/ # 工具函數和服務 +│ ├── services/ # API 服務層 +│ │ ├── flashcards.ts # 詞卡 API 服務 +│ │ └── auth.ts # 認證 API 服務 +│ └── utils/ # 工具函數 +├── public/ # 靜態資源 +│ └── images/ # 圖片資源 +├── package.json # 專案配置 +├── tailwind.config.js # Tailwind 配置 +├── tsconfig.json # TypeScript 配置 +└── next.config.js # Next.js 配置 +``` + +## 3. 頁面架構設計 + +### 3.1 詞卡管理頁面 (app/flashcards/page.tsx) + +#### 組件層級結構 +``` +FlashcardsPage (Protected Route Wrapper) +└── FlashcardsContent (Main Logic Component) + ├── Navigation (Top Navigation Bar) + ├── Page Header (Title + Action Buttons) + ├── Tab System (All Cards / Favorites) + ├── Search & Filter Section + │ ├── Main Search Input + │ ├── Advanced Filters (Collapsible) + │ └── Quick Filter Buttons + ├── Flashcard List Display + │ └── Flashcard Card Components (Repeated) + └── FlashcardForm Modal (When Editing) +``` + +#### 狀態管理架構 +```typescript +// 主要狀態 +const [activeTab, setActiveTab] = useState<'all-cards' | 'favorites'>('all-cards') +const [searchTerm, setSearchTerm] = useState('') +const [searchFilters, setSearchFilters] = useState({ + cefrLevel: '', + partOfSpeech: '', + masteryLevel: '', + onlyFavorites: false +}) + +// 資料狀態 +const [flashcards, setFlashcards] = useState([]) +const [loading, setLoading] = useState(true) +const [error, setError] = useState(null) + +// 表單狀態 +const [showForm, setShowForm] = useState(false) +const [editingCard, setEditingCard] = useState(null) +``` + +### 3.2 AI 分析頁面 (app/generate/page.tsx) + +#### 組件結構 +``` +GeneratePage (Protected Route Wrapper) +└── GenerateContent (Main Logic Component) + ├── Navigation + ├── Input Section (Conditional Rendering) + │ ├── Text Input Area + │ ├── Character Counter + │ └── Analysis Button + └── Analysis Results Section (Conditional Rendering) + ├── Star Explanation Banner + ├── Grammar Correction Panel (If Needed) + ├── Main Sentence Display + │ ├── Vocabulary Statistics Cards + │ ├── ClickableTextV2 Component + │ ├── Translation Section + │ └── Idioms Display Section + └── Action Buttons +``` + +#### 分析結果資料流 +```typescript +// AI 分析請求 +handleAnalyzeSentence() + → fetch('/api/ai/analyze-sentence') + → setSentenceAnalysis(apiData) + → setShowAnalysisView(true) + +// 詞卡保存流程 +handleSaveWord() + → flashcardsService.createFlashcard() + → alert(success/failure message) +``` + +## 4. 組件設計模式 + +### 4.1 可重用組件設計 + +#### ClickableTextV2 組件 +```typescript +interface ClickableTextProps { + text: string; // 顯示文字 + analysis?: Record; // 詞彙分析資料 + onWordClick?: (word: string, analysis: WordAnalysis) => void; + onSaveWord?: (word: string, analysis: WordAnalysis) => Promise<{success: boolean}>; + remainingUsage?: number; // 剩餘使用次數 + showIdiomsInline?: boolean; // 是否內嵌顯示慣用語 +} + +// 設計特色: +// - 詞彙點擊互動 +// - CEFR 等級顏色編碼 +// - 星星標記 (高頻詞彙) +// - 彈出式詞彙詳情 +// - Portal 渲染優化 +``` + +#### FlashcardForm 組件 +```typescript +interface FlashcardFormProps { + initialData?: Partial; // 編輯時的初始資料 + isEdit?: boolean; // 是否為編輯模式 + onSuccess: () => void; // 成功回調 + onCancel: () => void; // 取消回調 +} + +// 表單欄位: +// - word (必填) +// - translation (必填) +// - definition (必填) +// - pronunciation (選填) +// - partOfSpeech (選填,下拉選單) +// - example (必填) +// - exampleTranslation (選填) +``` + +### 4.2 路由保護模式 + +#### ProtectedRoute 組件 +```typescript +// 用途:保護需要認證的頁面 +export function ProtectedRoute({ children }: { children: React.ReactNode }) { + // 檢查認證狀態 + // 未登入時導向登入頁面 + // 已登入時顯示子組件 + return ( +
+ {/* 認證檢查邏輯 */} + {children} +
+ ) +} + +// 使用方式: + + + +``` + +## 5. API 服務層設計 + +### 5.1 服務類別架構 + +#### FlashcardsService 類別 +```typescript +class FlashcardsService { + private readonly baseURL = `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5008'}/api`; + + // 統一的請求處理方法 + private async makeRequest(endpoint: string, options: RequestInit = {}): Promise + + // CRUD 操作方法 + async getFlashcards(search?: string, favoritesOnly?: boolean): Promise> + async getFlashcard(id: string): Promise> + async createFlashcard(data: CreateFlashcardRequest): Promise> + async updateFlashcard(id: string, data: CreateFlashcardRequest): Promise> + async deleteFlashcard(id: string): Promise> + async toggleFavorite(id: string): Promise> +} + +// 單例模式匯出 +export const flashcardsService = new FlashcardsService(); +``` + +### 5.2 型別定義標準化 + +#### 核心型別定義 +```typescript +// 詞卡介面定義 +export interface Flashcard { + id: string; + word: string; + translation: string; + definition: string; + partOfSpeech: string; + pronunciation: string; + example: string; + exampleTranslation?: string; + masteryLevel: number; + timesReviewed: number; + isFavorite: boolean; + nextReviewDate: string; + difficultyLevel: string; + createdAt: string; + updatedAt?: string; +} + +// API 回應格式 +export interface ApiResponse { + success: boolean; + data?: T; + error?: string; + message?: string; +} + +// 創建請求格式 +export interface CreateFlashcardRequest { + word: string; + translation: string; + definition: string; + pronunciation: string; + partOfSpeech: string; + example: string; + exampleTranslation?: string; +} +``` + +## 6. 樣式系統架構 + +### 6.1 Tailwind CSS 配置 + +#### 主要設計系統 +```javascript +// tailwind.config.js +module.exports = { + content: ['./app/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: { + colors: { + primary: { + DEFAULT: '#3B82F6', // 主色調藍色 + hover: '#2563EB' // 懸停狀態 + } + } + } + } +} +``` + +#### CEFR 等級顏色系統 +```typescript +const getCEFRColor = (level: string) => { + switch (level) { + case 'A1': return 'bg-green-100 text-green-700 border-green-200' + case 'A2': return 'bg-blue-100 text-blue-700 border-blue-200' + case 'B1': return 'bg-yellow-100 text-yellow-700 border-yellow-200' + case 'B2': return 'bg-orange-100 text-orange-700 border-orange-200' + case 'C1': return 'bg-red-100 text-red-700 border-red-200' + case 'C2': return 'bg-purple-100 text-purple-700 border-purple-200' + default: return 'bg-gray-100 text-gray-700 border-gray-200' + } +} +``` + +### 6.2 響應式設計模式 + +#### 斷點策略 +```css +/* 手機版 */ +@media (max-width: 767px) { + .flashcard-grid { grid-template-columns: 1fr; } + .search-filters { flex-direction: column; } +} + +/* 平板版 */ +@media (min-width: 768px) and (max-width: 1023px) { + .flashcard-grid { grid-template-columns: repeat(2, 1fr); } +} + +/* 桌面版 */ +@media (min-width: 1024px) { + .flashcard-grid { grid-template-columns: repeat(3, 1fr); } +} +``` + +## 7. 效能優化策略 + +### 7.1 React 效能優化 + +#### useMemo 和 useCallback 使用 +```typescript +// 快取詞彙統計計算 +const vocabularyStats = useMemo(() => { + if (!sentenceAnalysis?.vocabularyAnalysis) return defaultStats; + + // 複雜計算邏輯 + return calculateStats(sentenceAnalysis.vocabularyAnalysis); +}, [sentenceAnalysis]) + +// 快取事件處理函數 +const handleWordClick = useCallback(async (word: string, event: React.MouseEvent) => { + // 事件處理邏輯 +}, [findWordAnalysis, onWordClick, calculatePopupPosition]) +``` + +#### 組件懶載入 +```typescript +// 動態導入大型組件 +const HeavyComponent = dynamic(() => import('./HeavyComponent'), { + loading: () =>
載入中...
, + ssr: false +}) +``` + +### 7.2 資料載入優化 + +#### 條件式資料載入 +```typescript +useEffect(() => { + // 只在需要時載入資料 + if (activeTab === 'favorites') { + loadFavoriteFlashcards(); + } else { + loadAllFlashcards(); + } +}, [activeTab]) +``` + +#### 錯誤邊界處理 +```typescript +// API 服務層錯誤處理 +private async makeRequest(endpoint: string, options: RequestInit = {}): Promise { + try { + const response = await fetch(`${this.baseURL}${endpoint}`, { + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + ...options, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: 'Network error' })); + throw new Error(errorData.error || `HTTP ${response.status}`); + } + + return response.json(); + } catch (error) { + console.error('API request failed:', error); + throw error; + } +} +``` + +## 8. 狀態管理模式 + +### 8.1 本地狀態管理 + +#### 頁面級狀態 +```typescript +// 每個頁面管理自己的狀態 +function FlashcardsContent() { + // UI 狀態 + const [activeTab, setActiveTab] = useState('all-cards') + const [showForm, setShowForm] = useState(false) + + // 資料狀態 + const [flashcards, setFlashcards] = useState([]) + const [loading, setLoading] = useState(true) + + // 搜尋狀態 + const [searchTerm, setSearchTerm] = useState('') + const [searchFilters, setSearchFilters] = useState(defaultFilters) +} +``` + +#### 跨組件狀態同步 +```typescript +// 通過 props 和 callback 實現父子組件通信 + setShowForm(false)} +/> +``` + +### 8.2 持久化狀態 + +#### localStorage 使用 +```typescript +// 用戶偏好設定 +const userLevel = localStorage.getItem('userEnglishLevel') || 'A2' + +// 搜尋歷史 (未來功能) +const searchHistory = JSON.parse(localStorage.getItem('searchHistory') || '[]') +``` + +## 9. 用戶體驗設計 + +### 9.1 載入狀態處理 + +#### 統一載入狀態 +```typescript +if (loading) { + return ( +
+
載入中...
+
+ ) +} +``` + +#### 按鈕載入狀態 +```typescript + +``` + +### 9.2 錯誤狀態處理 + +#### 頁面級錯誤 +```typescript +if (error) { + return ( +
+
{error}
+
+ ) +} +``` + +#### 表單錯誤 +```typescript +{error && ( +
+ {error} +
+)} +``` + +### 9.3 空狀態設計 + +#### 友善的空狀態 +```typescript +{filteredCards.length === 0 ? ( +
+

沒有找到詞卡

+ + 創建新詞卡 + +
+) : ( + // 詞卡列表 +)} +``` + +## 10. 互動設計模式 + +### 10.1 搜尋互動 + +#### 即時搜尋 +```typescript +// 輸入時即時過濾,無防抖延遲 +const filteredCards = allCards.filter(card => { + if (searchTerm) { + const searchLower = searchTerm.toLowerCase() + return card.word?.toLowerCase().includes(searchLower) || + card.translation?.toLowerCase().includes(searchLower) || + card.definition?.toLowerCase().includes(searchLower) + } + return true +}) +``` + +#### 搜尋結果高亮 +```typescript +const highlightSearchTerm = (text: string, searchTerm: string) => { + if (!searchTerm || !text) return text + + const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi') + const parts = text.split(regex) + + return parts.map((part, index) => + regex.test(part) ? ( + + {part} + + ) : part + ) +} +``` + +### 10.2 彈窗互動設計 + +#### Portal 渲染模式 +```typescript +import { createPortal } from 'react-dom' + +const VocabPopup = () => { + if (!selectedWord || !mounted) return null + + return createPortal( + <> + {/* 遮罩層 */} +
+ + {/* 彈窗內容 */} +
+ {/* 詞彙詳細資訊 */} +
+ , + document.body + ) +} +``` + +### 10.3 鍵盤操作支援 + +#### ESC 鍵清除搜尋 +```typescript + { + if (e.key === 'Escape') { + setSearchTerm('') + } + }} +/> +``` + +## 11. 開發與建置 + +### 11.1 開發環境 + +#### 開發伺服器啟動 +```bash +cd frontend +npm run dev + +# 開發伺服器運行於: http://localhost:3000 +# Hot Reload 啟用 +# Fast Refresh 啟用 +``` + +#### 環境變數配置 +```bash +# .env.local +NEXT_PUBLIC_API_URL=http://localhost:5008 +``` + +### 11.2 建置優化 + +#### Next.js 配置 (next.config.js) +```javascript +/** @type {import('next').NextConfig} */ +const nextConfig = { + experimental: { + appDir: true, // 啟用 App Router + }, + images: { + domains: ['localhost'], // 圖片域名白名單 + } +} + +module.exports = nextConfig +``` + +#### TypeScript 配置 (tsconfig.json) +```json +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "es6"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "baseUrl": ".", + "paths": { + "@/*": ["./*"] // 路徑別名 + } + } +} +``` + +## 12. 測試策略 + +### 12.1 組件測試 +```typescript +// 未來實作:Jest + React Testing Library +import { render, screen, fireEvent } from '@testing-library/react' +import { FlashcardForm } from './FlashcardForm' + +test('should submit form with valid data', async () => { + const onSuccess = jest.fn() + render( {}} />) + + // 填寫表單 + fireEvent.change(screen.getByLabelText('單字'), { target: { value: 'test' } }) + + // 提交表單 + fireEvent.click(screen.getByText('創建詞卡')) + + // 驗證結果 + expect(onSuccess).toHaveBeenCalled() +}) +``` + +### 12.2 API 服務測試 +```typescript +// Mock fetch 進行單元測試 +global.fetch = jest.fn() + +test('should create flashcard successfully', async () => { + const mockResponse = { success: true, data: mockFlashcard } + ;(fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => mockResponse, + }) + + const result = await flashcardsService.createFlashcard(mockData) + expect(result.success).toBe(true) +}) +``` + +--- + +**文檔版本**: v1.0 +**建立日期**: 2025-09-24 +**維護負責**: 前端開發團隊 +**下次審核**: 架構變更時 + +> 📋 相關文檔: +> - [系統架構總覽](./system-architecture.md) +> - [後端架構詳細說明](./backend-architecture.md) +> - [詞卡 API 規格](./flashcard-api-specification.md) \ No newline at end of file diff --git a/docs/04_technical/system-architecture.md b/docs/04_technical/system-architecture.md new file mode 100644 index 0000000..02019e2 --- /dev/null +++ b/docs/04_technical/system-architecture.md @@ -0,0 +1,185 @@ +# DramaLing 系統架構總覽 + +## 1. 系統架構概要 + +### 1.1 整體架構圖 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ │ │ │ │ │ +│ 前端 (React) │◄──►│ 後端 API │◄──►│ 外部服務 │ +│ Next.js 15 │ │ ASP.NET Core │ │ Google AI │ +│ TypeScript │ │ C# .NET 8 │ │ Azure Speech │ +│ │ │ │ │ │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ │ + │ 資料存儲 │ + │ SQLite DB │ + │ 本地檔案 │ + │ │ + └─────────────────┘ +``` + +### 1.2 核心組件 + +#### 🖥️ **前端層 (Client)** +- **技術棧**: Next.js 15 + TypeScript + Tailwind CSS +- **部署**: http://localhost:3000 (開發環境) +- **主要職責**: 用戶介面、用戶互動、API 呼叫 + +#### ⚙️ **後端層 (Server)** +- **技術棧**: ASP.NET Core 8.0 + C# +- **部署**: http://localhost:5008 (開發環境) +- **主要職責**: 業務邏輯、API 服務、資料處理 + +#### 💾 **資料層 (Data)** +- **資料庫**: SQLite + Entity Framework Core +- **檔案位置**: `dramaling_test.db` +- **主要職責**: 資料持久化、關聯管理 + +#### 🌐 **外部服務層 (External)** +- **AI 服務**: Google Gemini API +- **語音服務**: Azure Speech Service +- **主要職責**: AI 分析、語音合成 + +## 2. 技術棧詳細說明 + +### 2.1 前端技術棧 + +| 技術組件 | 版本 | 用途 | 檔案位置 | +|---------|------|------|----------| +| Next.js | 15.x | React 框架 | `/frontend` | +| TypeScript | 5.x | 型別安全 | `.tsx`, `.ts` 檔案 | +| Tailwind CSS | 3.x | 樣式框架 | `tailwind.config.js` | +| React | 18.x | UI 組件 | `/components` | + +### 2.2 後端技術棧 + +| 技術組件 | 版本 | 用途 | 檔案位置 | +|---------|------|------|----------| +| ASP.NET Core | 8.0 | Web API 框架 | `/backend/DramaLing.Api` | +| Entity Framework | 8.0 | ORM 框架 | `/Data` | +| SQLite | 3.x | 資料庫 | `dramaling_test.db` | +| JWT | - | 身份驗證 | `/Services/AuthService.cs` | + +### 2.3 開發工具 + +| 工具 | 用途 | 配置檔案 | +|------|------|----------| +| npm | 前端套件管理 | `package.json` | +| dotnet | 後端專案管理 | `*.csproj` | +| Git | 版本控制 | `.gitignore` | + +## 3. 服務間通信 + +### 3.1 前後端通信 +- **協議**: HTTP/HTTPS +- **格式**: JSON +- **認證**: JWT Token +- **CORS**: 配置允許 localhost:3000-3002 + +### 3.2 API 基礎規範 +- **基礎路徑**: `http://localhost:5008/api` +- **內容類型**: `application/json` +- **錯誤格式**: 標準化錯誤回應 +- **成功格式**: `{success: boolean, data?: T, error?: string}` + +## 4. 資料流架構 + +### 4.1 典型請求流程 + +``` +用戶操作 → React Component → API Service → HTTP Request → ASP.NET Controller → Business Service → Entity Framework → SQLite + ↓ ↑ +Response ← State Update ← API Response ← HTTP Response ← JSON Serialization ← Business Logic ← Database Query ←────────┘ +``` + +### 4.2 錯誤處理流程 + +``` +異常發生 → Exception Handling → Error Response → Frontend Error State → User Feedback +``` + +## 5. 安全架構 + +### 5.1 認證機制 +- **JWT Token**: 用戶身份驗證 +- **Token 來源**: Supabase 相容格式 +- **驗證位置**: ASP.NET Core Middleware + +### 5.2 資料保護 +- **輸入驗證**: 前端 + 後端雙重驗證 +- **SQL 注入防護**: Entity Framework 參數化查詢 +- **XSS 防護**: React 內建保護機制 + +## 6. 開發環境 + +### 6.1 本地開發設定 + +#### 前端開發伺服器 +```bash +cd frontend +npm run dev +# 運行於: http://localhost:3000 +``` + +#### 後端開發伺服器 +```bash +cd backend +dotnet run --project DramaLing.Api +# 運行於: http://localhost:5008 +``` + +### 6.2 環境變數 + +#### 後端環境變數 +```bash +DRAMALING_DB_CONNECTION=Data Source=dramaling_test.db +DRAMALING_SUPABASE_URL=https://localhost +DRAMALING_SUPABASE_JWT_SECRET=dev-secret-minimum-32-characters-long-for-jwt-signing-in-development-mode-only +USE_INMEMORY_DB=false +``` + +#### 前端環境變數 +```bash +NEXT_PUBLIC_API_URL=http://localhost:5008 +``` + +## 7. 部署架構 + +### 7.1 開發環境 +- **前端**: npm run dev (Hot Reload) +- **後端**: dotnet run (Hot Reload) +- **資料庫**: 本地 SQLite 檔案 + +### 7.2 生產環境 (未來) +- **前端**: Vercel / Netlify +- **後端**: Azure App Service / AWS EC2 +- **資料庫**: PostgreSQL / Azure SQL + +## 8. 監控與維護 + +### 8.1 日誌系統 +- **前端**: Console.log (開發), Sentry (生產) +- **後端**: ILogger, 結構化日誌 +- **API**: HTTP 請求/回應日誌 + +### 8.2 效能監控 +- **前端**: Next.js 內建分析 +- **後端**: ASP.NET Core 效能計數器 +- **資料庫**: EF Core 查詢分析 + +--- + +**文檔版本**: v1.0 +**建立日期**: 2025-09-24 +**維護負責**: 開發團隊 +**更新頻率**: 架構變更時即時更新 + +> 📋 此文檔為系統架構總覽,詳細技術規格請參考: +> - [後端架構詳細說明](./backend-architecture.md) +> - [前端架構詳細說明](./frontend-architecture.md) +> - [詞卡 API 規格](./flashcard-api-specification.md) \ No newline at end of file diff --git a/docs/02_design/AI句子分析規格/文件結構說明.md b/docs/archive/AI句子分析規格/文件結構說明.md similarity index 100% rename from docs/02_design/AI句子分析規格/文件結構說明.md rename to docs/archive/AI句子分析規格/文件結構說明.md diff --git a/docs/02_design/AI句子分析規格/系統整合與部署規格.md b/docs/archive/AI句子分析規格/系統整合與部署規格.md similarity index 100% rename from docs/02_design/AI句子分析規格/系統整合與部署規格.md rename to docs/archive/AI句子分析規格/系統整合與部署規格.md