# 複習系統技術實作規格書

**版本**: 1.0
**對應PRD**: 產品需求規格.md
**目標讀者**: 開發者、技術主管
**最後更新**: 2025-10-03

---

## 📊 **核心算法和公式**

### **進度條計算公式**
```typescript
進度百分比 = (今日完成) / (今日完成 + 今日到期) * 100
```

**實作範例**:
```typescript
const progressPercentage = completedToday / (completedToday + dueToday) * 100
```

### **複習間隔算法**
```typescript
下一次複習時間 = 當前時間 + (2^成功複習次數) 天
```

**實作範例**:
```typescript
const nextReviewDate = new Date()
nextReviewDate.setDate(nextReviewDate.getDate() + Math.pow(2, successCount))
```

---

## 🏷️ **卡片狀態管理機制**

### **延遲計數系統** (前端處理)

**數據結構設計**:
```typescript
interface CardState {
  id: string
  // 原始卡片數據...

  // 前端狀態計數器
  skipCount: number      // 跳過次數
  wrongCount: number     // 答錯次數
  successCount: number   // 答對次數
  isCompleted: boolean   // 是否已完成
  originalOrder: number  // 原始順序
}
```

**增加延遲計數的情況**:
1. **用戶點擊跳過** → `skipCount++`
2. **用戶答錯題目** → `wrongCount++`
3. **效果相同** → 都會降低該卡片的優先級

**前端排序邏輯** (您的需求):
```typescript
const sortCardsByPriority = (cards: CardState[]) => {
  return cards.sort((a, b) => {
    // 1. 已完成的卡片排到最後
    if (a.isCompleted && !b.isCompleted) return 1
    if (!a.isCompleted && b.isCompleted) return -1

    // 2. 未完成卡片按延遲分數排序
    const aDelayScore = a.skipCount + a.wrongCount
    const bDelayScore = b.skipCount + b.wrongCount

    // 延遲分數越低越前面 (被跳過次數少的優先)
    if (aDelayScore !== bDelayScore) {
      return aDelayScore - bDelayScore
    }

    // 3. 延遲分數相同時按原始順序
    return a.originalOrder - b.originalOrder
  })
}
```

**狀態更新邏輯**:
```typescript
// 跳過處理
const handleSkip = (cardIndex: number) => {
  setCards(prevCards =>
    prevCards.map((card, index) =>
      index === cardIndex
        ? { ...card, skipCount: card.skipCount + 1 }
        : card
    )
  )
  // 重新排序並移到下一張
  proceedToNext()
}

// 答錯處理
const handleWrongAnswer = (cardIndex: number) => {
  setCards(prevCards =>
    prevCards.map((card, index) =>
      index === cardIndex
        ? { ...card, wrongCount: card.wrongCount + 1 }
        : card
    )
  )
  proceedToNext()
}

// 答對處理 (完成卡片)
const handleCorrectAnswer = (cardIndex: number) => {
  setCards(prevCards =>
    prevCards.map((card, index) =>
      index === cardIndex
        ? { ...card, isCompleted: true, successCount: card.successCount + 1 }
        : card
    )
  )
  proceedToNext()
}
```

---

## 🎯 **測驗模式實作**

### **階段1: 翻卡記憶 (已實作)**

**數據結構**:
```typescript
interface FlashcardData {
  id: string
  word: string
  definition: string
  example: string
  exampleTranslation: string
  pronunciation: string
  synonyms: string[]
  cefr: string
}
```

**狀態管理**:
```typescript
const [isFlipped, setIsFlipped] = useState(false)
const [selectedConfidence, setSelectedConfidence] = useState<number | null>(null)
const [cardHeight, setCardHeight] = useState<number>(400)
```

**信心度評分映射** (簡化為3選項):
```typescript
const confidenceToScore = {
  1: 0,    // 模糊 → 答錯
  2: 1,    // 一般 → 答對
  3: 1     // 熟悉 → 答對
}

// 對應的UI配置
const confidenceOptions = [
  { level: 1, label: '模糊', color: 'bg-orange-100 text-orange-700 border-orange-200' },
  { level: 2, label: '一般', color: 'bg-yellow-100 text-yellow-700 border-yellow-200' },
  { level: 3, label: '熟悉', color: 'bg-green-100 text-green-700 border-green-200' }
]
```

### **階段2: 詞彙選擇題 (已設計完成)**

**組件架構** (基於您的設計):
```typescript
interface VocabChoiceTestProps {
  cardData: FlashcardData
  options: string[]         // 4選1選項
  onAnswer: (answer: string) => void
  onReportError: () => void
  disabled?: boolean
}

// 組件分區設計
const VocabChoiceTest = () => {
  const questionArea = // 問題顯示區域
  const optionsArea =  // 選項網格區域
  const resultArea =   // 結果顯示區域

  return (
    <ChoiceTestContainer
      questionArea={questionArea}
      optionsArea={optionsArea}
      resultArea={resultArea}
    />
  )
}
```

**問題顯示區域設計**:
```typescript
const questionArea = (
  <div className="text-center">
    <div className="bg-gray-50 rounded-lg p-6">
      <h3 className="font-semibold text-gray-900 mb-3 text-left">定義</h3>
      <p className="text-gray-700 text-left text-lg leading-relaxed">
        {cardData.definition}
      </p>
    </div>
    <p className="text-lg text-gray-700 mt-4 text-left">
      請選擇符合上述定義的英文詞彙：
    </p>
  </div>
)
```

**狀態管理設計**:
```typescript
const [selectedAnswer, setSelectedAnswer] = useState<string | null>(null)
const [showResult, setShowResult] = useState(false)

// 答案驗證邏輯
const isCorrect = useMemo(() =>
  selectedAnswer === cardData.word,
  [selectedAnswer, cardData.word]
)

// 選擇處理
const handleAnswerSelect = useCallback((answer: string) => {
  if (disabled || showResult) return
  setSelectedAnswer(answer)
  setShowResult(true)
  onAnswer(answer)
}, [disabled, showResult, onAnswer])
```

**選項生成算法**:
```typescript
// MVP階段: 固定選項
const generateSimpleOptions = (correctWord: string): string[] => {
  const fixedDistractors = ['apple', 'orange', 'banana']
  return shuffle([correctWord, ...fixedDistractors])
}

// 階段3升級方案: 智能干擾項
const generateSmartOptions = (correctWord: Flashcard, allWords: Flashcard[]): string[] => {
  const samePOS = allWords.filter(w => w.partOfSpeech === correctWord.partOfSpeech)
  const sameCEFR = allWords.filter(w => w.cefr === correctWord.cefr)
  const distractors = selectRandom([...samePOS, ...sameCEFR], 3)
  return shuffle([correctWord.word, ...distractors.map(w => w.word)])
}
```

**ChoiceGrid組件設計** (您的完整設計):
```typescript
interface ChoiceGridProps {
  options: string[]           // 4個選項
  selectedOption?: string | null
  correctAnswer?: string
  showResult?: boolean       // 控制結果顯示
  onSelect: (option: string) => void
  disabled?: boolean
  className?: string
}

// 響應式網格布局
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
  {options.map((option, index) => (
    <ChoiceOption
      key={`${option}-${index}`}
      option={option}
      index={index}
      isSelected={selectedOption === option}
      isCorrect={showResult && option === correctAnswer}
      isIncorrect={showResult && option !== correctAnswer}
      showResult={showResult}
      onSelect={onSelect}
      disabled={disabled}
    />
  ))}
</div>
```

**ChoiceOption樣式規格** (您的設計):
```typescript
// 選項按鈕狀態樣式
const getOptionStyles = (isSelected, isCorrect, isIncorrect, showResult) => {
  if (showResult) {
    if (isCorrect) return 'border-green-500 bg-green-50 text-green-700'
    if (isIncorrect && isSelected) return 'border-red-500 bg-red-50 text-red-700'
    return 'border-gray-200 bg-gray-50 text-gray-500'
  }

  if (isSelected) return 'border-blue-500 bg-blue-50 text-blue-700'
  return 'border-gray-200 hover:border-blue-300 hover:bg-blue-50'
}

// 按鈕基礎樣式
className={`p-4 text-center rounded-lg border-2 transition-all ${getOptionStyles()}`}
```

**UI組件層次設計**:
```typescript
// 您設計的完整組件結構
<ChoiceTestContainer>          // 外層容器和錯誤處理
  <TestHeader title="詞彙選擇" cefr={cardData.cefr} />
  <QuestionArea>               // 問題顯示區域
    <div className="bg-gray-50 rounded-lg p-6">
      <h3>定義</h3>
      <p className="text-lg leading-relaxed">{definition}</p>
    </div>
    <p className="text-lg mt-4">請選擇符合上述定義的英文詞彙：</p>
  </QuestionArea>
  <OptionsArea>                // 選項區域
    <ChoiceGrid                // 響應式2x2網格
      options={options}
      selectedOption={selectedAnswer}
      correctAnswer={cardData.word}
      showResult={showResult}
      onSelect={handleAnswerSelect}
      disabled={disabled}
    />
  </OptionsArea>
  <ResultArea>                 // 結果顯示區域
    {showResult && (
      <TestResultDisplay
        isCorrect={isCorrect}
        correctAnswer={cardData.word}
        userAnswer={selectedAnswer}
        pronunciation={cardData.pronunciation}
        example={cardData.example}
      />
    )}
  </ResultArea>
</ChoiceTestContainer>
```

---

## 🎨 **UI/UX實作規格**

### **翻卡動畫參數**
```css
/* 3D翻卡動畫 - 已調校的完美參數 */
.flip-card {
  perspective: 1000px;
  transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}

.flip-card.flipped {
  transform: rotateY(180deg);
}
```

### **響應式高度計算**
```typescript
const calculateCardHeight = () => {
  const backHeight = backRef.current?.scrollHeight || 0
  const minHeight = window.innerWidth <= 480 ? 300 :
                   window.innerWidth <= 768 ? 350 : 400
  return Math.max(minHeight, backHeight)
}
```

### **信心度按鈕配色**
```typescript
const confidenceColors = {
  1: 'bg-red-100 text-red-700 border-red-200',      // 完全不懂
  2: 'bg-orange-100 text-orange-700 border-orange-200', // 模糊
  3: 'bg-yellow-100 text-yellow-700 border-yellow-200', // 一般
  4: 'bg-blue-100 text-blue-700 border-blue-200',   // 熟悉
  5: 'bg-green-100 text-green-700 border-green-200'  // 非常熟悉
}
```

---

## 📱 **API設計規格** (階段3實作)

### **獲取複習卡片API**
```typescript
GET /api/flashcards/due?limit=10

Response:
{
  "success": true,
  "data": {
    "flashcards": FlashcardData[],
    "count": number
  },
  "timestamp": string
}
```

### **記錄複習結果API**
```typescript
POST /api/flashcards/{id}/review

Request:
{
  "confidence": number,  // 1-5
  "isCorrect": boolean,  // 基於confidence >= 3判斷
  "reviewType": "flip-memory",
  "responseTimeMs": number
}

Response:
{
  "success": boolean,
  "nextReviewDate": string,
  "newSuccessCount": number
}
```

---

## 🗄️ **數據存儲設計**

### **階段1: 內存狀態 (當前)**
```typescript
// 純React狀態，會話結束即消失
const [currentIndex, setCurrentIndex] = useState(0)
const [score, setScore] = useState({ correct: 0, total: 0 })
```

### **階段2: 本地存儲 (計劃)**
```typescript
// localStorage持久化
const saveProgress = (progress: ReviewProgress) => {
  localStorage.setItem('review-progress', JSON.stringify({
    currentIndex,
    score,
    timestamp: Date.now()
  }))
}
```

### **階段3: 後端同步 (遠期)**
```typescript
// 與後端API同步
const syncProgress = async (progress: ReviewProgress) => {
  await api.post('/api/user/review-progress', progress)
}
```

---

## 🔒 **業務邏輯約束**

### **狀態轉換規則**
```typescript
// 卡片狀態轉換
enum CardState {
  PENDING = 'pending',     // 未開始
  COMPLETED = 'completed', // 已完成
  DELAYED = 'delayed'      // 延遲 (跳過或答錯)
}

// 狀態轉換邏輯
const handleAnswer = (confidence: number) => {
  if (confidence >= 3) {
    // 答對: 移除延遲標記，標記完成
    card.state = CardState.COMPLETED
    card.isDelayed = false
    card.successCount++
  } else {
    // 答錯: 添加延遲標記
    card.isDelayed = true
    card.state = CardState.DELAYED
  }
}
```

### **隊列管理邏輯**
```typescript
// 下一張卡片選擇邏輯
const getNextCard = (cards: FlashCard[]) => {
  // 1. 優先選擇未延遲的卡片
  const normalCards = cards.filter(c => !c.isDelayed && c.state === CardState.PENDING)
  if (normalCards.length > 0) {
    return normalCards[0]
  }

  // 2. 如果沒有正常卡片，選擇延遲卡片
  const delayedCards = cards.filter(c => c.isDelayed)
  return delayedCards[0] || null
}
```

---

## ⚡ **性能和優化規格**

### **載入性能要求**
```typescript
// 性能指標
const PERFORMANCE_TARGETS = {
  INITIAL_LOAD: 2000,      // 初始載入 < 2秒
  CARD_FLIP: 500,          // 翻卡響應 < 500ms
  CONFIDENCE_SELECT: 200,  // 按鈕響應 < 200ms
  NAVIGATION: 300          // 頁面切換 < 300ms
}
```

### **記憶體管理**
```typescript
// 避免記憶體洩漏
useEffect(() => {
  const timer = setTimeout(updateHeight, 100)
  const resizeHandler = () => updateHeight()

  window.addEventListener('resize', resizeHandler)

  return () => {
    clearTimeout(timer)
    window.removeEventListener('resize', resizeHandler)
  }
}, [dependencies])
```

---

## 🧪 **測試規格** (TDD Implementation)

### **測試覆蓋要求**
- **核心邏輯**: 100% 測試覆蓋
- **UI組件**: 關鍵交互測試
- **API調用**: Mock測試
- **狀態管理**: 狀態轉換測試

### **測試案例範例**
```typescript
// 翻卡邏輯測試
describe('FlipCard Component', () => {
  test('應該在點擊時切換翻轉狀態', () => {
    const { result } = renderHook(() => useFlipCard())

    act(() => result.current.flip())
    expect(result.current.isFlipped).toBe(true)
  })

  test('應該在選擇信心度時觸發提交', () => {
    const onSubmit = jest.fn()
    const card = render(<FlipCard onSubmit={onSubmit} />)

    card.selectConfidence(4)
    expect(onSubmit).toHaveBeenCalledWith(4)
  })
})
```

---

## 📋 **實作檢查清單**

### **每個功能完成時檢查**
- [ ] 功能符合產品需求規格
- [ ] 遵循技術約束和算法
- [ ] 通過所有相關測試
- [ ] 性能指標達標
- [ ] 無記憶體洩漏
- [ ] 錯誤處理完善

### **代碼品質標準**
- [ ] TypeScript 無錯誤
- [ ] ESLint 無警告
- [ ] 組件 < 200行
- [ ] 函數 < 20行
- [ ] 嵌套層次 < 3層

---

*技術實作規格維護者: 開發團隊*
*版本控制: 與產品需求規格同步更新*
*目的: 確保實作準確性，避免開發失控*