diff --git a/Cloudflare-R2圖片儲存遷移指南.md b/Cloudflare-R2圖片儲存遷移指南.md
new file mode 100644
index 0000000..687a5d1
--- /dev/null
+++ b/Cloudflare-R2圖片儲存遷移指南.md
@@ -0,0 +1,594 @@
+# Cloudflare R2 圖片儲存遷移指南
+
+## 概述
+
+將 DramaLing 後端的例句圖片儲存從本地檔案系統遷移到 Cloudflare R2 雲端儲存服務。
+
+## 目前架構分析
+
+### 現有圖片儲存系統
+- **接口**: `IImageStorageService`
+- **實現**: `LocalImageStorageService`
+- **儲存位置**: `wwwroot/images/examples`
+- **URL 格式**: `https://localhost:5008/images/examples/{fileName}`
+- **依賴注入**: 已在 `ServiceCollectionExtensions.cs` 注冊
+
+### 系統優點
+✅ 良好的抽象設計,便於替換實現
+✅ 完整的接口定義,包含所有必要操作
+✅ 已整合到圖片生成工作流程中
+
+## Phase 1: Cloudflare R2 環境準備
+
+### 1.1 建立 R2 Bucket
+
+1. **登入 Cloudflare Dashboard**
+ - 前往 https://dash.cloudflare.com/
+ - 選擇你的帳戶
+
+2. **建立 R2 Bucket**
+ ```
+ 左側導航 → R2 Object Storage → Create bucket
+
+ Bucket 名稱: dramaling-images
+ 區域: 建議選擇離用戶較近的區域 (如 Asia-Pacific)
+ ```
+
+### 1.2 設定 API 憑證
+
+1. **建立 R2 API Token**
+ ```
+ R2 Dashboard → Manage R2 API tokens → Create API token
+
+ Permission: Object Read & Write
+ Bucket: dramaling-images
+ TTL: 永不過期 (或根據需求設定)
+ ```
+
+2. **記錄重要資訊**
+ ```
+ Access Key ID: [記錄此值]
+ Secret Access Key: [記錄此值]
+ Account ID: [從 R2 Dashboard 右側取得]
+ Bucket Name: dramaling-images
+ Endpoint URL: https://[account-id].r2.cloudflarestorage.com
+ ```
+
+### 1.3 設定 CORS (跨域存取)
+
+在 R2 Dashboard → dramaling-images → Settings → CORS policy:
+
+```json
+[
+ {
+ "AllowedOrigins": [
+ "http://localhost:3000",
+ "http://localhost:5000",
+ "https://你的前端域名.com"
+ ],
+ "AllowedMethods": ["GET", "HEAD"],
+ "AllowedHeaders": ["*"],
+ "ExposeHeaders": [],
+ "MaxAgeSeconds": 86400
+ }
+]
+```
+
+### 1.4 設定 Public URL (可選)
+
+如果需要 CDN 加速:
+```
+R2 Dashboard → dramaling-images → Settings → Public URL
+Connect Custom Domain: images.dramaling.com (需要你有 Cloudflare 管理的域名)
+```
+
+## Phase 2: .NET 專案設定
+
+### 2.1 安裝 NuGet 套件
+
+在 `DramaLing.Api.csproj` 中添加:
+
+```xml
+
+
+```
+
+或使用 Package Manager Console:
+```powershell
+dotnet add package AWSSDK.S3
+dotnet add package AWSSDK.Extensions.NETCore.Setup
+```
+
+### 2.2 設定模型類別
+
+建立 `backend/DramaLing.Api/Models/Configuration/CloudflareR2Options.cs`:
+
+```csharp
+namespace DramaLing.Api.Models.Configuration;
+
+public class CloudflareR2Options
+{
+ public const string SectionName = "CloudflareR2";
+
+ public string AccessKeyId { get; set; } = string.Empty;
+ public string SecretAccessKey { get; set; } = string.Empty;
+ public string AccountId { get; set; } = string.Empty;
+ public string BucketName { get; set; } = string.Empty;
+ public string EndpointUrl { get; set; } = string.Empty;
+ public string PublicUrlBase { get; set; } = string.Empty; // 用於 CDN URL
+ public bool UsePublicUrl { get; set; } = false;
+}
+
+public class CloudflareR2OptionsValidator : IValidateOptions
+{
+ public ValidateOptionsResult Validate(string name, CloudflareR2Options options)
+ {
+ var failures = new List();
+
+ if (string.IsNullOrEmpty(options.AccessKeyId))
+ failures.Add("CloudflareR2:AccessKeyId is required");
+
+ if (string.IsNullOrEmpty(options.SecretAccessKey))
+ failures.Add("CloudflareR2:SecretAccessKey is required");
+
+ if (string.IsNullOrEmpty(options.AccountId))
+ failures.Add("CloudflareR2:AccountId is required");
+
+ if (string.IsNullOrEmpty(options.BucketName))
+ failures.Add("CloudflareR2:BucketName is required");
+
+ if (string.IsNullOrEmpty(options.EndpointUrl))
+ failures.Add("CloudflareR2:EndpointUrl is required");
+
+ return failures.Count > 0
+ ? ValidateOptionsResult.Fail(failures)
+ : ValidateOptionsResult.Success;
+ }
+}
+```
+
+## Phase 3: 實現 R2 儲存服務
+
+### 3.1 建立 R2ImageStorageService
+
+建立 `backend/DramaLing.Api/Services/Media/Storage/R2ImageStorageService.cs`:
+
+```csharp
+using Amazon.S3;
+using Amazon.S3.Model;
+using DramaLing.Api.Models.Configuration;
+using DramaLing.Api.Services.Storage;
+using Microsoft.Extensions.Options;
+
+namespace DramaLing.Api.Services.Media.Storage;
+
+public class R2ImageStorageService : IImageStorageService
+{
+ private readonly AmazonS3Client _s3Client;
+ private readonly CloudflareR2Options _options;
+ private readonly ILogger _logger;
+
+ public R2ImageStorageService(
+ IOptions options,
+ ILogger logger)
+ {
+ _options = options.Value ?? throw new ArgumentNullException(nameof(options));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+
+ // 設定 S3 Client 連接 Cloudflare R2
+ var config = new AmazonS3Config
+ {
+ ServiceURL = _options.EndpointUrl,
+ ForcePathStyle = true, // R2 要求使用 Path Style
+ UseHttp = false // 強制 HTTPS
+ };
+
+ _s3Client = new AmazonS3Client(_options.AccessKeyId, _options.SecretAccessKey, config);
+
+ _logger.LogInformation("R2ImageStorageService initialized with bucket: {BucketName}",
+ _options.BucketName);
+ }
+
+ public async Task SaveImageAsync(Stream imageStream, string fileName)
+ {
+ try
+ {
+ var key = $"examples/{fileName}"; // R2 中的檔案路徑
+
+ var request = new PutObjectRequest
+ {
+ BucketName = _options.BucketName,
+ Key = key,
+ InputStream = imageStream,
+ ContentType = GetContentType(fileName),
+ CannedACL = S3CannedACL.PublicRead // 設定為公開讀取
+ };
+
+ var response = await _s3Client.PutObjectAsync(request);
+
+ if (response.HttpStatusCode == System.Net.HttpStatusCode.OK)
+ {
+ _logger.LogInformation("Image uploaded successfully to R2: {Key}", key);
+ return key; // 回傳 R2 中的檔案路徑
+ }
+
+ throw new Exception($"Upload failed with status: {response.HttpStatusCode}");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to save image to R2: {FileName}", fileName);
+ throw;
+ }
+ }
+
+ public Task GetImageUrlAsync(string imagePath)
+ {
+ // 如果設定了 CDN 域名,使用公開 URL
+ if (_options.UsePublicUrl && !string.IsNullOrEmpty(_options.PublicUrlBase))
+ {
+ var publicUrl = $"{_options.PublicUrlBase.TrimEnd('/')}/{imagePath.TrimStart('/')}";
+ return Task.FromResult(publicUrl);
+ }
+
+ // 否則使用 R2 直接 URL
+ var r2Url = $"{_options.EndpointUrl.TrimEnd('/')}/{_options.BucketName}/{imagePath.TrimStart('/')}";
+ return Task.FromResult(r2Url);
+ }
+
+ public async Task DeleteImageAsync(string imagePath)
+ {
+ try
+ {
+ var request = new DeleteObjectRequest
+ {
+ BucketName = _options.BucketName,
+ Key = imagePath
+ };
+
+ var response = await _s3Client.DeleteObjectAsync(request);
+
+ _logger.LogInformation("Image deleted from R2: {Key}", imagePath);
+ return response.HttpStatusCode == System.Net.HttpStatusCode.NoContent;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to delete image from R2: {ImagePath}", imagePath);
+ return false;
+ }
+ }
+
+ public async Task ImageExistsAsync(string imagePath)
+ {
+ try
+ {
+ var request = new GetObjectMetadataRequest
+ {
+ BucketName = _options.BucketName,
+ Key = imagePath
+ };
+
+ await _s3Client.GetObjectMetadataAsync(request);
+ return true;
+ }
+ catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
+ {
+ return false;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to check image existence in R2: {ImagePath}", imagePath);
+ return false;
+ }
+ }
+
+ public async Task GetStorageInfoAsync()
+ {
+ try
+ {
+ // 取得 bucket 資訊 (簡化版本,R2 API 限制較多)
+ var listRequest = new ListObjectsV2Request
+ {
+ BucketName = _options.BucketName,
+ Prefix = "examples/",
+ MaxKeys = 1000 // 限制查詢數量避免超時
+ };
+
+ var response = await _s3Client.ListObjectsV2Async(listRequest);
+
+ var totalSize = response.S3Objects.Sum(obj => obj.Size);
+ var fileCount = response.S3Objects.Count;
+
+ return new StorageInfo
+ {
+ Provider = "Cloudflare R2",
+ TotalSizeBytes = totalSize,
+ FileCount = fileCount,
+ Status = "Available"
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to get R2 storage info");
+ return new StorageInfo
+ {
+ Provider = "Cloudflare R2",
+ Status = $"Error: {ex.Message}"
+ };
+ }
+ }
+
+ private static string GetContentType(string fileName)
+ {
+ var extension = Path.GetExtension(fileName).ToLowerInvariant();
+ return extension switch
+ {
+ ".jpg" or ".jpeg" => "image/jpeg",
+ ".png" => "image/png",
+ ".gif" => "image/gif",
+ ".webp" => "image/webp",
+ _ => "application/octet-stream"
+ };
+ }
+
+ public void Dispose()
+ {
+ _s3Client?.Dispose();
+ }
+}
+```
+
+## Phase 4: 更新應用配置
+
+### 4.1 更新 ServiceCollectionExtensions.cs
+
+修改 `AddBusinessServices` 方法:
+
+```csharp
+public static IServiceCollection AddBusinessServices(this IServiceCollection services, IConfiguration configuration)
+{
+ services.AddScoped();
+
+ // 媒體服務
+ services.AddScoped();
+
+ // 圖片儲存服務 - 根據設定選擇實現
+ var useR2Storage = configuration.GetValue("CloudflareR2:Enabled", false);
+
+ if (useR2Storage)
+ {
+ // 配置 Cloudflare R2 選項
+ services.Configure(configuration.GetSection(CloudflareR2Options.SectionName));
+ services.AddSingleton, CloudflareR2OptionsValidator>();
+
+ // 注冊 R2 服務
+ services.AddScoped();
+
+ // AWS SDK 設定 (R2 相容 S3 API)
+ services.AddAWSService();
+ }
+ else
+ {
+ // 使用本地儲存
+ services.AddScoped();
+ }
+
+ // 其他服務保持不變...
+ return services;
+}
+```
+
+### 4.2 更新 appsettings.json
+
+```json
+{
+ "CloudflareR2": {
+ "Enabled": false,
+ "AccessKeyId": "", // 從環境變數載入
+ "SecretAccessKey": "", // 從環境變數載入
+ "AccountId": "", // 從環境變數載入
+ "BucketName": "dramaling-images",
+ "EndpointUrl": "", // 會從 AccountId 計算
+ "PublicUrlBase": "", // 如果有設定 CDN 域名
+ "UsePublicUrl": false
+ },
+ "ImageStorage": {
+ "Local": {
+ "BasePath": "wwwroot/images/examples",
+ "BaseUrl": "https://localhost:5008/images/examples"
+ }
+ }
+}
+```
+
+### 4.3 生產環境配置 (appsettings.Production.json)
+
+```json
+{
+ "CloudflareR2": {
+ "Enabled": true,
+ "BucketName": "dramaling-images",
+ "EndpointUrl": "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",
+ "PublicUrlBase": "https://images.dramaling.com", // 如果設定了 CDN
+ "UsePublicUrl": true
+ }
+}
+```
+
+## Phase 5: 環境變數設定
+
+### 5.1 開發環境 (.env 或 user secrets)
+
+```bash
+# Cloudflare R2 設定
+CLOUDFLARE_R2_ACCESS_KEY_ID=你的AccessKeyId
+CLOUDFLARE_R2_SECRET_ACCESS_KEY=你的SecretAccessKey
+CLOUDFLARE_R2_ACCOUNT_ID=你的AccountId
+```
+
+### 5.2 生產環境 (Render 環境變數)
+
+在 Render Dashboard 設定以下環境變數:
+
+```
+CLOUDFLARE_R2_ACCESS_KEY_ID=實際的AccessKeyId
+CLOUDFLARE_R2_SECRET_ACCESS_KEY=實際的SecretAccessKey
+CLOUDFLARE_R2_ACCOUNT_ID=實際的AccountId
+```
+
+### 5.3 配置載入邏輯
+
+在 `Program.cs` 中添加環境變數覆蓋:
+
+```csharp
+// 在 builder.Services.Configure 之前添加
+builder.Configuration.AddInMemoryCollection(new Dictionary
+{
+ ["CloudflareR2:AccessKeyId"] = Environment.GetEnvironmentVariable("CLOUDFLARE_R2_ACCESS_KEY_ID") ?? "",
+ ["CloudflareR2:SecretAccessKey"] = Environment.GetEnvironmentVariable("CLOUDFLARE_R2_SECRET_ACCESS_KEY") ?? "",
+ ["CloudflareR2:AccountId"] = Environment.GetEnvironmentVariable("CLOUDFLARE_R2_ACCOUNT_ID") ?? ""
+});
+
+// 動態計算 EndpointUrl
+var accountId = Environment.GetEnvironmentVariable("CLOUDFLARE_R2_ACCOUNT_ID");
+if (!string.IsNullOrEmpty(accountId))
+{
+ builder.Configuration["CloudflareR2:EndpointUrl"] = $"https://{accountId}.r2.cloudflarestorage.com";
+}
+```
+
+## Phase 6: 測試和部署
+
+### 6.1 本地測試步驟
+
+1. **設定環境變數**
+ ```bash
+ export CLOUDFLARE_R2_ACCESS_KEY_ID=你的AccessKeyId
+ export CLOUDFLARE_R2_SECRET_ACCESS_KEY=你的SecretAccessKey
+ export CLOUDFLARE_R2_ACCOUNT_ID=你的AccountId
+ ```
+
+2. **修改 appsettings.Development.json**
+ ```json
+ {
+ "CloudflareR2": {
+ "Enabled": true
+ }
+ }
+ ```
+
+3. **測試圖片生成功能**
+ - 前往 AI 生成頁面
+ - 分析句子並生成例句圖
+ - 檢查圖片是否正確上傳到 R2
+ - 檢查圖片 URL 是否可正常存取
+
+### 6.2 驗證清單
+
+- [ ] R2 Bucket 中出現新圖片
+- [ ] 圖片 URL 可在瀏覽器中正常開啟
+- [ ] 前端可正確顯示 R2 圖片
+- [ ] 圖片刪除功能正常
+- [ ] 錯誤處理和日誌記錄正常
+
+### 6.3 回滾計劃
+
+如果需要回滾到本地儲存:
+
+1. **修改設定**
+ ```json
+ {
+ "CloudflareR2": {
+ "Enabled": false
+ }
+ }
+ ```
+
+2. **重啟應用**
+ - 系統自動切換回 LocalImageStorageService
+
+## Phase 7: 生產環境部署
+
+### 7.1 Render 部署設定
+
+1. **設定環境變數**
+ - 在 Render Dashboard 設定上述的環境變數
+
+2. **更新生產配置**
+ ```json
+ {
+ "CloudflareR2": {
+ "Enabled": true
+ }
+ }
+ ```
+
+3. **重新部署應用**
+
+### 7.2 CDN 設定 (可選)
+
+如果需要 CDN 加速:
+
+1. **設定 Custom Domain**
+ ```
+ Cloudflare Dashboard → 你的域名 → DNS → Add record:
+ Type: CNAME
+ Name: images
+ Content: YOUR_ACCOUNT_ID.r2.cloudflarestorage.com
+ ```
+
+2. **更新應用設定**
+ ```json
+ {
+ "CloudflareR2": {
+ "PublicUrlBase": "https://images.yourdomain.com",
+ "UsePublicUrl": true
+ }
+ }
+ ```
+
+## 成本效益分析
+
+### Cloudflare R2 優勢
+- **成本效益**: 無 egress 費用
+- **效能**: CDN 全球加速
+- **可靠性**: 99.999999999% 耐久性
+- **擴展性**: 無限容量
+- **相容性**: S3 API 相容
+
+### 預期成本 (以1000張圖片為例)
+- **儲存費用**: ~$0.015/GB/月
+- **操作費用**: $4.50/百萬次請求
+- **CDN**: 免費 (Cloudflare 域名)
+
+## 注意事項
+
+1. **圖片命名**: 保持現有的檔案命名邏輯
+2. **錯誤處理**: 網路問題時的重試機制
+3. **快取**: 考慮前端圖片快取策略
+4. **安全性**: API 金鑰務必使用環境變數
+5. **監控**: 設定 R2 使用量監控
+
+## 實施時間表
+
+- **Phase 1-2**: 1-2 小時 (環境準備)
+- **Phase 3**: 2-3 小時 (代碼實現)
+- **Phase 4-5**: 1 小時 (設定和測試)
+- **Phase 6-7**: 1 小時 (部署和驗證)
+
+**總計**: 約 5-7 小時完成完整遷移
+
+## 檔案清單
+
+### 新增檔案
+- `Models/Configuration/CloudflareR2Options.cs`
+- `Services/Media/Storage/R2ImageStorageService.cs`
+
+### 修改檔案
+- `Extensions/ServiceCollectionExtensions.cs`
+- `appsettings.json`
+- `appsettings.Production.json`
+- `DramaLing.Api.csproj`
+
+### 環境設定
+- Cloudflare R2 Dashboard 設定
+- Render 環境變數設定
\ No newline at end of file
diff --git a/Generate頁面UX改善計劃.md b/Generate頁面UX改善計劃.md
new file mode 100644
index 0000000..6bef873
--- /dev/null
+++ b/Generate頁面UX改善計劃.md
@@ -0,0 +1,228 @@
+# Generate 頁面 UX 改善計劃
+
+## 🎯 問題描述
+
+### 目前的問題
+當用戶在 `http://localhost:3001/generate` 頁面輸入英文文本進行分析後:
+
+1. **第一次分析**:用戶輸入文本 → 點擊「分析句子」→ 下方顯示分析結果 ✅
+2. **想要分析新文本時**:用戶在輸入框中輸入新文本 → **舊的分析結果仍然顯示** ❌
+3. **用戶體驗問題**:新輸入的文本和下方顯示的舊分析結果不匹配,造成混淆
+
+### 期望的使用流程
+1. 用戶輸入文本
+2. 點擊「分析句子」→ 顯示對應的分析結果
+3. 當用戶開始輸入**新文本**時 → **自動清除舊的分析結果**
+4. 用戶需要再次點擊「分析句子」才會顯示新文本的分析結果
+
+---
+
+## 🔧 解決方案
+
+### 核心改善邏輯
+添加**智能清除機制**:當用戶開始修改輸入文本時,自動清除之前的分析結果,避免新輸入和舊結果的不匹配。
+
+### 技術實現方案
+
+#### 1. **新增狀態管理**
+```typescript
+// 新增以下狀態
+const [lastAnalyzedText, setLastAnalyzedText] = useState('')
+const [isInitialLoad, setIsInitialLoad] = useState(true)
+```
+
+#### 2. **實現清除邏輯**
+```typescript
+// 監聽文本輸入變化
+useEffect(() => {
+ // 如果不是初始載入,且文本與上次分析的不同
+ if (!isInitialLoad && textInput !== lastAnalyzedText) {
+ // 清除分析結果
+ setSentenceAnalysis(null)
+ setSentenceMeaning('')
+ setGrammarCorrection(null)
+ setSelectedIdiom(null)
+ setSelectedWord(null)
+ }
+}, [textInput, lastAnalyzedText, isInitialLoad])
+```
+
+#### 3. **修改分析函數**
+```typescript
+const handleAnalyzeSentence = async () => {
+ // ... 現有邏輯 ...
+
+ // 分析成功後,記錄此次分析的文本
+ setLastAnalyzedText(textInput)
+ setIsInitialLoad(false)
+
+ // ... 其他邏輯 ...
+}
+```
+
+#### 4. **優化快取邏輯**
+```typescript
+// 恢復快取時標記為初始載入
+useEffect(() => {
+ const cached = loadAnalysisFromCache()
+ if (cached) {
+ setTextInput(cached.textInput || '')
+ setLastAnalyzedText(cached.textInput || '') // 同步記錄
+ setSentenceAnalysis(cached.sentenceAnalysis || null)
+ setSentenceMeaning(cached.sentenceMeaning || '')
+ setGrammarCorrection(cached.grammarCorrection || null)
+ setIsInitialLoad(false) // 標記快取載入完成
+ console.log('✅ 已恢復快取的分析結果')
+ } else {
+ setIsInitialLoad(false) // 沒有快取也要標記載入完成
+ }
+}, [loadAnalysisFromCache])
+```
+
+---
+
+## 📋 詳細修改步驟
+
+### 步驟 1:新增狀態變數
+在 `GenerateContent` 函數中新增:
+```typescript
+const [lastAnalyzedText, setLastAnalyzedText] = useState('')
+const [isInitialLoad, setIsInitialLoad] = useState(true)
+```
+
+### 步驟 2:添加文本變化監聽
+在現有的 `useEffect` 後添加新的 `useEffect`:
+```typescript
+// 監聽文本變化,自動清除不匹配的分析結果
+useEffect(() => {
+ if (!isInitialLoad && textInput !== lastAnalyzedText && sentenceAnalysis) {
+ // 清除所有分析結果
+ setSentenceAnalysis(null)
+ setSentenceMeaning('')
+ setGrammarCorrection(null)
+ setSelectedIdiom(null)
+ setSelectedWord(null)
+ console.log('🧹 已清除舊的分析結果,因為文本已改變')
+ }
+}, [textInput, lastAnalyzedText, isInitialLoad, sentenceAnalysis])
+```
+
+### 步驟 3:修改 `handleAnalyzeSentence` 函數
+在分析成功後添加:
+```typescript
+// 在 setSentenceAnalysis(analysisData) 之後添加
+setLastAnalyzedText(textInput)
+setIsInitialLoad(false)
+```
+
+### 步驟 4:修改快取恢復邏輯
+更新現有的快取恢復 `useEffect`:
+```typescript
+useEffect(() => {
+ const cached = loadAnalysisFromCache()
+ if (cached) {
+ setTextInput(cached.textInput || '')
+ setLastAnalyzedText(cached.textInput || '') // 新增這行
+ setSentenceAnalysis(cached.sentenceAnalysis || null)
+ setSentenceMeaning(cached.sentenceMeaning || '')
+ setGrammarCorrection(cached.grammarCorrection || null)
+ console.log('✅ 已恢復快取的分析結果')
+ }
+ setIsInitialLoad(false) // 新增這行,標記載入完成
+}, [loadAnalysisFromCache])
+```
+
+### 步驟 5:優化用戶體驗(可選)
+在分析結果區域添加提示訊息,當沒有分析結果時顯示:
+```typescript
+{/* 在分析結果區域前添加 */}
+{!sentenceAnalysis && textInput && (
+
+
💡
+
請點擊「分析句子」查看文本的詳細分析
+
+)}
+```
+
+---
+
+## 🎨 預期效果
+
+### 修改前(問題)
+1. 用戶輸入 "Hello world" → 分析 → 顯示結果
+2. 用戶修改為 "Good morning" → **舊的 "Hello world" 分析結果仍然顯示** ❌
+3. 造成混淆:新輸入 vs 舊結果不匹配
+
+### 修改後(解決)
+1. 用戶輸入 "Hello world" → 分析 → 顯示結果
+2. 用戶修改為 "Good morning" → **自動清除舊分析結果** ✅
+3. 用戶點擊「分析句子」→ 顯示 "Good morning" 的新分析結果 ✅
+
+---
+
+## 🔍 技術細節
+
+### 狀態管理邏輯
+- **`lastAnalyzedText`**: 記錄上次成功分析的文本內容
+- **`isInitialLoad`**: 區分頁面初始載入和用戶操作,避免載入時誤清除快取
+- **清除條件**: `textInput !== lastAnalyzedText` 且不是初始載入狀態
+
+### 快取兼容性
+- ✅ 保持現有的 localStorage 快取機制
+- ✅ 頁面重新載入時正確恢復分析結果
+- ✅ 只在用戶主動修改文本時才清除結果
+
+### 邊界情況處理
+- **頁面載入時**: 不會意外清除快取的分析結果
+- **空文本**: 當用戶清空輸入框時,分析結果會被清除
+- **相同文本**: 如果用戶修改後又改回原來的文本,不會重複清除
+
+---
+
+## 📁 需要修改的文件
+
+### 主要文件
+- **`frontend/app/generate/page.tsx`** - 實現所有邏輯修改
+
+### 修改範圍
+- 新增狀態變數 (2 行)
+- 新增 useEffect 監聽 (約 10 行)
+- 修改分析函數 (2 行)
+- 修改快取邏輯 (2 行)
+- 可選的 UI 提示 (約 8 行)
+
+**總計**: 約 25 行代碼修改,影響範圍小,風險低
+
+---
+
+## ✅ 驗收標準
+
+### 功能驗收
+1. ✅ 用戶輸入文本並分析後,修改輸入時舊結果立即消失
+2. ✅ 頁面重新載入時,快取的分析結果正確恢復
+3. ✅ 分析按鈕的狀態管理保持正常(loading、disabled 等)
+4. ✅ 語法修正面板的交互功能不受影響
+
+### 用戶體驗驗收
+1. ✅ 新輸入和分析結果始終保持一致
+2. ✅ 沒有意外的結果清除或誤操作
+3. ✅ 清晰的視覺反饋,用戶知道何時需要重新分析
+
+---
+
+## 🚀 實施建議
+
+### 開發順序
+1. **先實現核心邏輯** - 狀態管理和清除機制
+2. **測試基本功能** - 確保清除邏輯正常運作
+3. **優化快取邏輯** - 確保快取恢復不受影響
+4. **添加用戶提示** - 提升用戶體驗
+5. **全面測試** - 驗收所有功能點
+
+### 測試重點
+- 多次輸入不同文本的分析流程
+- 頁面重新載入的快取恢復
+- 語法修正功能的正常運作
+- 詞彙彈窗和保存功能的正常運作
+
+這個改善方案將顯著提升 Generate 頁面的用戶體驗,避免輸入和分析結果不匹配的混淆問題。
\ No newline at end of file
diff --git a/Google-Cloud-Storage圖片儲存遷移手冊.md b/Google-Cloud-Storage圖片儲存遷移手冊.md
new file mode 100644
index 0000000..327b39c
--- /dev/null
+++ b/Google-Cloud-Storage圖片儲存遷移手冊.md
@@ -0,0 +1,931 @@
+# Google Cloud Storage 圖片儲存遷移手冊
+
+## 概述
+
+將 DramaLing 後端的例句圖片儲存從本地檔案系統遷移到 Google Cloud Storage (GCS),利用 Google 的全球 CDN 網路提供更快的圖片載入速度和更高的可靠性。
+
+## 目前系統分析
+
+### 現有架構優勢
+- ✅ 使用 `IImageStorageService` 接口抽象化
+- ✅ 依賴注入已完整設定
+- ✅ 支援條件式服務切換
+- ✅ 完整的錯誤處理和日誌
+
+### 當前實現
+- **服務**: `LocalImageStorageService`
+- **儲存位置**: `wwwroot/images/examples`
+- **URL 模式**: `https://localhost:5008/images/examples/{fileName}`
+
+## Phase 1: Google Cloud 環境準備
+
+### 1.1 建立 Google Cloud 專案
+
+1. **前往 Google Cloud Console**
+ ```
+ 訪問: https://console.cloud.google.com/
+ 登入你的 Google 帳戶
+ ```
+
+2. **建立新專案**
+ ```
+ 點擊頂部專案選擇器 → 新增專案
+
+ 專案名稱: dramaling-storage (或你偏好的名稱)
+ 組織: 選擇適當的組織 (可選)
+ 專案 ID: 記錄此 ID,後續會用到
+ ```
+
+### 1.2 啟用 Cloud Storage API
+
+```
+Google Cloud Console → API 和服務 → 程式庫
+搜尋: "Cloud Storage API"
+點擊 → 啟用
+```
+
+### 1.3 建立 Service Account
+
+1. **建立服務帳戶**
+ ```
+ Google Cloud Console → IAM 和管理 → 服務帳戶 → 建立服務帳戶
+
+ 服務帳戶名稱: dramaling-storage-service
+ 說明: DramaLing application storage service account
+ ```
+
+2. **設定權限**
+ ```
+ 選擇角色: Storage Object Admin (允許完整的物件管理)
+ 或更細緻的權限:
+ - Storage Object Creator (建立物件)
+ - Storage Object Viewer (檢視物件)
+ - Storage Object Admin (完整管理)
+ ```
+
+3. **建立和下載金鑰檔案**
+ ```
+ 服務帳戶 → 金鑰 → 新增金鑰 → JSON
+
+ 下載 JSON 檔案並妥善保存
+ 檔案名建議: dramaling-storage-service-account.json
+ ```
+
+### 1.4 建立 Storage Bucket
+
+1. **建立 Bucket**
+ ```
+ Google Cloud Console → Cloud Storage → 瀏覽器 → 建立值區
+
+ 值區名稱: dramaling-images (需全球唯一)
+ 位置類型: Region
+ 位置: asia-east1 (台灣) 或 asia-southeast1 (新加坡)
+ 儲存類別: Standard
+ 存取控制: 統一 (Uniform)
+ ```
+
+2. **設定公開存取權限**
+ ```
+ 選擇建立的 bucket → 權限 → 新增主體
+
+ 新主體: allUsers
+ 角色: Storage Object Viewer
+
+ 這會讓圖片可以透過 URL 公開存取
+ ```
+
+### 1.5 設定 CORS
+
+在 Google Cloud Console 中設定 CORS:
+
+```bash
+# 建立 cors.json 檔案
+[
+ {
+ "origin": ["http://localhost:3000", "http://localhost:5000", "https://你的域名.com"],
+ "method": ["GET", "HEAD"],
+ "responseHeader": ["Content-Type"],
+ "maxAgeSeconds": 86400
+ }
+]
+
+# 使用 gsutil 設定 (需要安裝 Google Cloud SDK)
+gsutil cors set cors.json gs://dramaling-images
+```
+
+## Phase 2: .NET 專案設定
+
+### 2.1 安裝 NuGet 套件
+
+在 `backend/DramaLing.Api/DramaLing.Api.csproj` 中添加:
+
+```xml
+
+
+```
+
+或使用命令列:
+```bash
+cd backend/DramaLing.Api
+dotnet add package Google.Cloud.Storage.V1
+dotnet add package Google.Apis.Auth
+```
+
+### 2.2 建立配置模型
+
+建立 `backend/DramaLing.Api/Models/Configuration/GoogleCloudStorageOptions.cs`:
+
+```csharp
+using Microsoft.Extensions.Options;
+
+namespace DramaLing.Api.Models.Configuration;
+
+public class GoogleCloudStorageOptions
+{
+ public const string SectionName = "GoogleCloudStorage";
+
+ ///
+ /// Google Cloud 專案 ID
+ ///
+ public string ProjectId { get; set; } = string.Empty;
+
+ ///
+ /// Storage Bucket 名稱
+ ///
+ public string BucketName { get; set; } = string.Empty;
+
+ ///
+ /// Service Account JSON 金鑰檔案路徑
+ ///
+ public string CredentialsPath { get; set; } = string.Empty;
+
+ ///
+ /// Service Account JSON 金鑰內容 (用於環境變數)
+ ///
+ public string CredentialsJson { get; set; } = string.Empty;
+
+ ///
+ /// 自訂域名 (用於 CDN)
+ ///
+ public string CustomDomain { get; set; } = string.Empty;
+
+ ///
+ /// 是否使用自訂域名
+ ///
+ public bool UseCustomDomain { get; set; } = false;
+
+ ///
+ /// 圖片路徑前綴
+ ///
+ public string PathPrefix { get; set; } = "examples";
+}
+
+public class GoogleCloudStorageOptionsValidator : IValidateOptions
+{
+ public ValidateOptionsResult Validate(string name, GoogleCloudStorageOptions options)
+ {
+ var failures = new List();
+
+ if (string.IsNullOrEmpty(options.ProjectId))
+ failures.Add("GoogleCloudStorage:ProjectId is required");
+
+ if (string.IsNullOrEmpty(options.BucketName))
+ failures.Add("GoogleCloudStorage:BucketName is required");
+
+ if (string.IsNullOrEmpty(options.CredentialsPath) && string.IsNullOrEmpty(options.CredentialsJson))
+ failures.Add("Either GoogleCloudStorage:CredentialsPath or GoogleCloudStorage:CredentialsJson must be provided");
+
+ return failures.Count > 0
+ ? ValidateOptionsResult.Fail(failures)
+ : ValidateOptionsResult.Success;
+ }
+}
+```
+
+## Phase 3: 實現 Google Cloud Storage 服務
+
+### 3.1 建立 GoogleCloudImageStorageService
+
+建立 `backend/DramaLing.Api/Services/Media/Storage/GoogleCloudImageStorageService.cs`:
+
+```csharp
+using Google.Cloud.Storage.V1;
+using Google.Apis.Auth.OAuth2;
+using DramaLing.Api.Models.Configuration;
+using DramaLing.Api.Services.Storage;
+using Microsoft.Extensions.Options;
+using System.Text;
+
+namespace DramaLing.Api.Services.Media.Storage;
+
+public class GoogleCloudImageStorageService : IImageStorageService
+{
+ private readonly StorageClient _storageClient;
+ private readonly GoogleCloudStorageOptions _options;
+ private readonly ILogger _logger;
+
+ public GoogleCloudImageStorageService(
+ IOptions options,
+ ILogger logger)
+ {
+ _options = options.Value ?? throw new ArgumentNullException(nameof(options));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+
+ // 初始化 Storage Client
+ _storageClient = CreateStorageClient();
+
+ _logger.LogInformation("GoogleCloudImageStorageService initialized with bucket: {BucketName}",
+ _options.BucketName);
+ }
+
+ private StorageClient CreateStorageClient()
+ {
+ GoogleCredential credential;
+
+ // 優先使用 JSON 字串 (適合 Render 等雲端部署)
+ if (!string.IsNullOrEmpty(_options.CredentialsJson))
+ {
+ credential = GoogleCredential.FromJson(_options.CredentialsJson);
+ }
+ // 次要使用檔案路徑 (適合本地開發)
+ else if (!string.IsNullOrEmpty(_options.CredentialsPath) && File.Exists(_options.CredentialsPath))
+ {
+ credential = GoogleCredential.FromFile(_options.CredentialsPath);
+ }
+ // 最後嘗試使用預設認證 (適合 Google Cloud 環境)
+ else
+ {
+ credential = GoogleCredential.GetApplicationDefault();
+ }
+
+ return StorageClient.Create(credential);
+ }
+
+ public async Task SaveImageAsync(Stream imageStream, string fileName)
+ {
+ try
+ {
+ var objectName = $"{_options.PathPrefix}/{fileName}";
+
+ var obj = new Google.Cloud.Storage.V1.Object
+ {
+ Bucket = _options.BucketName,
+ Name = objectName,
+ ContentType = GetContentType(fileName)
+ };
+
+ // 上傳檔案
+ var uploadedObject = await _storageClient.UploadObjectAsync(obj, imageStream);
+
+ _logger.LogInformation("Image uploaded successfully to GCS: {ObjectName}", objectName);
+
+ return objectName; // 回傳 GCS 中的物件名稱
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to save image to GCS: {FileName}", fileName);
+ throw;
+ }
+ }
+
+ public Task GetImageUrlAsync(string imagePath)
+ {
+ // 如果設定了自訂域名 (CDN)
+ if (_options.UseCustomDomain && !string.IsNullOrEmpty(_options.CustomDomain))
+ {
+ var cdnUrl = $"https://{_options.CustomDomain.TrimEnd('/')}/{imagePath.TrimStart('/')}";
+ return Task.FromResult(cdnUrl);
+ }
+
+ // 使用標準 Google Cloud Storage URL
+ var gcsUrl = $"https://storage.googleapis.com/{_options.BucketName}/{imagePath.TrimStart('/')}";
+ return Task.FromResult(gcsUrl);
+ }
+
+ public async Task DeleteImageAsync(string imagePath)
+ {
+ try
+ {
+ await _storageClient.DeleteObjectAsync(_options.BucketName, imagePath);
+
+ _logger.LogInformation("Image deleted from GCS: {ObjectName}", imagePath);
+ return true;
+ }
+ catch (GoogleApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.NotFound)
+ {
+ _logger.LogWarning("Attempted to delete non-existent image: {ObjectName}", imagePath);
+ return false;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to delete image from GCS: {ObjectName}", imagePath);
+ return false;
+ }
+ }
+
+ public async Task ImageExistsAsync(string imagePath)
+ {
+ try
+ {
+ var obj = await _storageClient.GetObjectAsync(_options.BucketName, imagePath);
+ return obj != null;
+ }
+ catch (GoogleApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.NotFound)
+ {
+ return false;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to check image existence in GCS: {ObjectName}", imagePath);
+ return false;
+ }
+ }
+
+ public async Task GetStorageInfoAsync()
+ {
+ try
+ {
+ var request = new ListObjectsOptions
+ {
+ Prefix = _options.PathPrefix,
+ PageSize = 1000 // 限制查詢數量
+ };
+
+ var objects = _storageClient.ListObjectsAsync(_options.BucketName, request);
+
+ long totalSize = 0;
+ int fileCount = 0;
+
+ await foreach (var obj in objects)
+ {
+ totalSize += (long)(obj.Size ?? 0);
+ fileCount++;
+ }
+
+ return new StorageInfo
+ {
+ Provider = "Google Cloud Storage",
+ TotalSizeBytes = totalSize,
+ FileCount = fileCount,
+ Status = "Available"
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to get GCS storage info");
+ return new StorageInfo
+ {
+ Provider = "Google Cloud Storage",
+ Status = $"Error: {ex.Message}"
+ };
+ }
+ }
+
+ private static string GetContentType(string fileName)
+ {
+ var extension = Path.GetExtension(fileName).ToLowerInvariant();
+ return extension switch
+ {
+ ".jpg" or ".jpeg" => "image/jpeg",
+ ".png" => "image/png",
+ ".gif" => "image/gif",
+ ".webp" => "image/webp",
+ ".svg" => "image/svg+xml",
+ _ => "application/octet-stream"
+ };
+ }
+}
+```
+
+## Phase 4: 應用配置更新
+
+### 4.1 更新 ServiceCollectionExtensions.cs
+
+修改 `AddBusinessServices` 方法:
+
+```csharp
+///
+/// 配置業務服務
+///
+public static IServiceCollection AddBusinessServices(this IServiceCollection services, IConfiguration configuration)
+{
+ services.AddScoped();
+
+ // 媒體服務
+ services.AddScoped();
+
+ // 圖片儲存服務 - 根據設定選擇實現
+ var storageProvider = configuration.GetValue("ImageStorage:Provider", "Local");
+
+ switch (storageProvider.ToLowerInvariant())
+ {
+ case "googlecloud" or "gcs":
+ ConfigureGoogleCloudStorage(services, configuration);
+ break;
+
+ case "local":
+ default:
+ services.AddScoped();
+ break;
+ }
+
+ // 其他服務保持不變...
+ services.AddHttpClient();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+
+ return services;
+}
+
+private static void ConfigureGoogleCloudStorage(IServiceCollection services, IConfiguration configuration)
+{
+ // 配置 Google Cloud Storage 選項
+ services.Configure(configuration.GetSection(GoogleCloudStorageOptions.SectionName));
+ services.AddSingleton, GoogleCloudStorageOptionsValidator>();
+
+ // 註冊 Google Cloud Storage 服務
+ services.AddScoped();
+}
+```
+
+### 4.2 更新 appsettings.json
+
+```json
+{
+ "ImageStorage": {
+ "Provider": "Local",
+ "Local": {
+ "BasePath": "wwwroot/images/examples",
+ "BaseUrl": "https://localhost:5008/images/examples"
+ }
+ },
+ "GoogleCloudStorage": {
+ "ProjectId": "",
+ "BucketName": "dramaling-images",
+ "CredentialsPath": "",
+ "CredentialsJson": "",
+ "CustomDomain": "",
+ "UseCustomDomain": false,
+ "PathPrefix": "examples"
+ }
+}
+```
+
+### 4.3 開發環境設定 (appsettings.Development.json)
+
+```json
+{
+ "ImageStorage": {
+ "Provider": "GoogleCloud"
+ },
+ "GoogleCloudStorage": {
+ "ProjectId": "your-project-id",
+ "BucketName": "dramaling-images",
+ "CredentialsPath": "path/to/your/service-account.json",
+ "PathPrefix": "examples"
+ }
+}
+```
+
+### 4.4 生產環境設定 (appsettings.Production.json)
+
+```json
+{
+ "ImageStorage": {
+ "Provider": "GoogleCloud"
+ },
+ "GoogleCloudStorage": {
+ "ProjectId": "your-production-project-id",
+ "BucketName": "dramaling-images-prod",
+ "CustomDomain": "images.dramaling.com",
+ "UseCustomDomain": true,
+ "PathPrefix": "examples"
+ }
+}
+```
+
+## Phase 5: 認證設定
+
+### 5.1 本地開發環境
+
+**方法 1: Service Account JSON 檔案**
+
+1. **儲存金鑰檔案**
+ ```
+ 將下載的 JSON 檔案放到安全位置
+ 建議: backend/secrets/dramaling-storage-service-account.json
+
+ ⚠️ 務必將 secrets/ 目錄加入 .gitignore
+ ```
+
+2. **設定檔案路徑**
+ ```json
+ {
+ "GoogleCloudStorage": {
+ "CredentialsPath": "secrets/dramaling-storage-service-account.json"
+ }
+ }
+ ```
+
+**方法 2: 環境變數 (推薦)**
+
+```bash
+export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
+```
+
+**方法 3: User Secrets (最安全)**
+
+```bash
+cd backend/DramaLing.Api
+dotnet user-secrets init
+dotnet user-secrets set "GoogleCloudStorage:ProjectId" "your-project-id"
+dotnet user-secrets set "GoogleCloudStorage:CredentialsJson" "$(cat path/to/service-account.json)"
+```
+
+### 5.2 生產環境 (Render)
+
+在 Render Dashboard 設定環境變數:
+
+```
+GOOGLE_CLOUD_PROJECT_ID=your-project-id
+GOOGLE_CLOUD_STORAGE_BUCKET=dramaling-images-prod
+GOOGLE_CLOUD_CREDENTIALS_JSON=[整個JSON檔案內容]
+```
+
+然後在 `Program.cs` 中添加環境變數載入:
+
+```csharp
+// 在建立 builder 後添加
+builder.Configuration.AddInMemoryCollection(new Dictionary
+{
+ ["GoogleCloudStorage:ProjectId"] = Environment.GetEnvironmentVariable("GOOGLE_CLOUD_PROJECT_ID") ?? "",
+ ["GoogleCloudStorage:BucketName"] = Environment.GetEnvironmentVariable("GOOGLE_CLOUD_STORAGE_BUCKET") ?? "",
+ ["GoogleCloudStorage:CredentialsJson"] = Environment.GetEnvironmentVariable("GOOGLE_CLOUD_CREDENTIALS_JSON") ?? ""
+}!);
+```
+
+## Phase 6: 測試和驗證
+
+### 6.1 本地測試步驟
+
+1. **設定開發環境**
+ ```bash
+ # 設定環境變數
+ export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
+
+ # 或使用 user secrets
+ cd backend/DramaLing.Api
+ dotnet user-secrets set "GoogleCloudStorage:ProjectId" "your-project-id"
+ dotnet user-secrets set "GoogleCloudStorage:BucketName" "dramaling-images"
+ ```
+
+2. **修改開發設定**
+ ```json
+ {
+ "ImageStorage": {
+ "Provider": "GoogleCloud"
+ }
+ }
+ ```
+
+3. **測試圖片功能**
+ - 啟動後端 API
+ - 前往 AI 生成頁面
+ - 輸入句子並生成例句圖
+ - 檢查 Google Cloud Console 中的 bucket 是否有新檔案
+ - 檢查前端是否正確顯示圖片
+
+### 6.2 功能驗證清單
+
+- [ ] **圖片上傳**: 新圖片出現在 GCS bucket 中
+- [ ] **圖片顯示**: 前端可正確載入並顯示 GCS 圖片
+- [ ] **URL 生成**: 圖片 URL 格式正確
+- [ ] **圖片刪除**: 刪除功能正常運作
+- [ ] **錯誤處理**: 網路錯誤時有適當的錯誤訊息
+- [ ] **日誌記錄**: 操作日誌正確記錄
+- [ ] **效能**: 圖片載入速度合理
+
+### 6.3 常見問題排除
+
+**問題 1**: `The Application Default Credentials are not available`
+```
+解決方法:
+1. 檢查環境變數 GOOGLE_APPLICATION_CREDENTIALS 是否設定
+2. 檢查 JSON 檔案路徑是否正確
+3. 檢查 JSON 檔案格式是否正確
+```
+
+**問題 2**: `Access denied` 錯誤
+```
+解決方法:
+1. 檢查 Service Account 是否有 Storage Object Admin 權限
+2. 檢查 bucket 名稱是否正確
+3. 檢查專案 ID 是否正確
+```
+
+**問題 3**: CORS 錯誤
+```
+解決方法:
+1. 設定 bucket 的 CORS 政策
+2. 檢查前端域名是否在允許清單中
+```
+
+## Phase 7: 生產環境部署
+
+### 7.1 Render 環境設定
+
+1. **設定環境變數**
+ ```
+ 在 Render Dashboard → Your Service → Environment:
+
+ GOOGLE_CLOUD_PROJECT_ID = your-production-project-id
+ GOOGLE_CLOUD_STORAGE_BUCKET = dramaling-images-prod
+ GOOGLE_CLOUD_CREDENTIALS_JSON = [完整的JSON內容,單行格式]
+ ```
+
+2. **JSON 內容格式化**
+ ```bash
+ # 將多行 JSON 轉為單行 (用於環境變數)
+ cat service-account.json | jq -c .
+ ```
+
+### 7.2 CDN 設定 (可選)
+
+如果需要 CDN 加速:
+
+1. **設定 Load Balancer**
+ ```
+ Google Cloud Console → 網路服務 → Cloud CDN
+ 建立 HTTP(S) Load Balancer
+ 後端指向你的 Storage bucket
+ ```
+
+2. **自訂域名設定**
+ ```json
+ {
+ "GoogleCloudStorage": {
+ "CustomDomain": "images.dramaling.com",
+ "UseCustomDomain": true
+ }
+ }
+ ```
+
+### 7.3 部署流程
+
+1. **更新生產設定**
+ ```json
+ {
+ "ImageStorage": {
+ "Provider": "GoogleCloud"
+ }
+ }
+ ```
+
+2. **部署到 Render**
+ - 推送代碼到 Git
+ - Render 自動部署
+ - 檢查部署日誌
+
+3. **驗證功能**
+ - 測試圖片生成
+ - 檢查 GCS bucket
+ - 測試圖片載入速度
+
+## 成本分析
+
+### Google Cloud Storage 定價 (2024年價格)
+
+- **Storage**: $0.020 per GB/month (Standard class, Asia region)
+- **Operations**:
+ - Class A (write): $0.05 per 10,000 operations
+ - Class B (read): $0.004 per 10,000 operations
+- **Network**:
+ - Asia to Asia: $0.05 per GB
+ - Global CDN: $0.08-0.20 per GB (depending on region)
+
+### 預期成本估算 (1000 張圖片範例)
+
+假設每張圖片 500KB:
+- **儲存成本**: 0.5GB × $0.02 = $0.01/月
+- **上傳操作**: 1000 × $0.05/10,000 = $0.005
+- **瀏覽操作**: 10,000 次 × $0.004/10,000 = $0.004
+
+**每月總成本約**: $0.02-0.05 USD (非常便宜)
+
+### 與其他方案比較
+
+| 方案 | 月成本 | 效能 | 可靠性 | 管理複雜度 |
+|------|-------|------|--------|------------|
+| 本地儲存 | $0 | 低 | 低 | 高 |
+| Google Cloud | $0.02-0.05 | 高 | 高 | 低 |
+| AWS S3 | $0.03-0.08 | 高 | 高 | 中 |
+| Cloudflare R2 | $0.01-0.03 | 高 | 高 | 低 |
+
+## 遷移時程表
+
+### 建議實施順序
+
+1. **準備階段** (1-2 小時):
+ - 建立 Google Cloud 專案
+ - 設定 Service Account 和 Bucket
+ - 下載認證檔案
+
+2. **開發階段** (2-3 小時):
+ - 安裝 NuGet 套件
+ - 實現 GoogleCloudImageStorageService
+ - 建立配置模型
+
+3. **測試階段** (1-2 小時):
+ - 本地環境測試
+ - 功能驗證
+ - 效能測試
+
+4. **部署階段** (1 小時):
+ - 設定生產環境變數
+ - 部署到 Render
+ - 最終驗證
+
+**總計時間**: 5-8 小時
+
+## 安全性考量
+
+### 最佳實務
+
+1. **認證管理**:
+ - ✅ 使用環境變數存放敏感資訊
+ - ✅ 本地使用 user secrets
+ - ✅ 生產使用 Render 環境變數
+ - ❌ 絕不將金鑰提交到 Git
+
+2. **權限管理**:
+ - ✅ Service Account 最小權限原則
+ - ✅ Bucket 層級的權限控制
+ - ✅ 定期輪換 Service Account 金鑰
+
+3. **網路安全**:
+ - ✅ 使用 HTTPS 傳輸
+ - ✅ 設定 CORS 限制
+ - ✅ 監控異常存取
+
+## 回滾策略
+
+如果需要回到本地儲存:
+
+1. **快速回滾**
+ ```json
+ {
+ "ImageStorage": {
+ "Provider": "Local"
+ }
+ }
+ ```
+
+2. **重新部署**
+ - 系統自動切換回 LocalImageStorageService
+
+3. **資料遷移** (可選)
+ - 從 GCS 下載圖片回本地 (如果需要)
+
+## 監控和維護
+
+### 日誌監控
+
+- 設定 Google Cloud Logging 監控
+- 關注 Storage API 錯誤率
+- 監控上傳/下載效能
+
+### 成本監控
+
+- 設定 Google Cloud 計費警告
+- 定期檢查 Storage 使用量
+- 監控 API 調用頻率
+
+### 維護建議
+
+- 定期檢查圖片存取權限
+- 清理未使用的圖片 (可選)
+- 備份重要圖片 (可選)
+
+## 技術支援
+
+### 文檔資源
+- [Google Cloud Storage .NET SDK](https://cloud.google.com/storage/docs/reference/libraries#client-libraries-install-csharp)
+- [Service Account 認證](https://cloud.google.com/docs/authentication/production)
+- [Storage 最佳實務](https://cloud.google.com/storage/docs/best-practices)
+
+### 故障排除指令
+
+```bash
+# 檢查 GCS 連線
+gsutil ls gs://your-bucket-name
+
+# 測試認證
+gcloud auth application-default print-access-token
+
+# 檢查 bucket 權限
+gsutil iam get gs://your-bucket-name
+```
+
+---
+
+## 實施檢查清單
+
+### 準備階段
+- [ ] 建立 Google Cloud 專案
+- [ ] 啟用 Cloud Storage API
+- [ ] 建立 Service Account
+- [ ] 下載 JSON 認證檔案
+- [ ] 建立 Storage Bucket
+- [ ] 設定 Bucket 權限和 CORS
+
+### 開發階段
+- [x] 安裝 Google.Cloud.Storage.V1 NuGet 套件 ✅ **已完成 2024-10-08**
+- [x] 建立 GoogleCloudStorageOptions 配置模型 ✅ **已完成 2024-10-08**
+- [x] 實現 GoogleCloudImageStorageService ✅ **已完成 2024-10-08**
+- [x] 更新 ServiceCollectionExtensions ✅ **已完成 2024-10-08**
+- [x] 更新 appsettings.json 配置 ✅ **已完成 2024-10-08**
+- [x] 編譯測試通過 ✅ **已完成 2024-10-08**
+- [ ] 設定本地認證
+
+### 測試階段
+- [ ] 本地環境測試圖片上傳
+- [ ] 驗證圖片 URL 可存取
+- [ ] 測試圖片刪除功能
+- [ ] 檢查錯誤處理
+- [ ] 驗證日誌記錄
+
+### 部署階段
+- [ ] 設定 Render 環境變數
+- [ ] 更新生產配置
+- [ ] 部署並驗證功能
+- [ ] 設定監控和警告
+- [ ] 準備回滾計劃
+
+## 🚀 實施進度
+
+### 已完成項目 (2024-10-08)
+
+✅ **NuGet 套件安裝**
+- 已在 `DramaLing.Api.csproj` 中添加:
+ - `Google.Cloud.Storage.V1` v4.7.0
+ - `Google.Apis.Auth` v1.68.0
+
+✅ **配置模型建立**
+- 已建立 `Models/Configuration/GoogleCloudStorageOptions.cs`
+- 支援多種認證方式:Service Account JSON、檔案路徑、API Key
+- 包含配置驗證器
+
+✅ **服務實現完成**
+- 已建立 `Services/Media/Storage/GoogleCloudImageStorageService.cs`
+- 完整實現 `IImageStorageService` 接口
+- 支援現有的 User Secrets 中的 `GoogleStorage:ApiKey`
+- 包含錯誤處理和日誌記錄
+
+### 設計特色 ⭐
+
+🔄 **條件式切換支援**:
+- 可透過設定檔在本地儲存 ↔ Google Cloud 之間切換
+- 零程式碼修改,完全向後相容
+- 支援開發/測試/生產環境不同配置
+
+🔐 **多重認證支援**:
+- Service Account JSON (推薦)
+- 檔案路徑認證
+- 現有 API Key 支援
+- 環境變數配置
+
+✅ **依賴注入設定完成**
+- 已在 `ServiceCollectionExtensions.cs` 中添加條件式切換邏輯
+- 支援通過 `ImageStorage:Provider` 配置選擇實現
+- 已更新 `Program.cs` 傳入 configuration 參數
+
+✅ **編譯測試通過**
+- **Build succeeded with 0 Error(s)**
+- 所有組件整合成功
+- 準備就緒可進行實際測試
+
+### 🎯 當前狀態
+
+**系統已具備完整的抽換式圖片儲存架構!**
+
+**立即可用的切換方式**:
+```json
+// 保持本地儲存 (當前設定)
+"ImageStorage": { "Provider": "Local" }
+
+// 切換到 Google Cloud Storage
+"ImageStorage": { "Provider": "GoogleCloud" }
+```
+
+### 下一步 (準備實際使用)
+
+要啟用 Google Cloud Storage:
+1. 建立 Google Cloud 專案和 Bucket
+2. 設定認證 (`gcloud auth application-default login`)
+3. 修改設定檔 `Provider` 為 `GoogleCloud`
+
+**抽換式架構開發完成!** 🚀
+
+這份手冊提供了完整的 Google Cloud Storage 遷移指南,讓你能夠安全且有效地從本地儲存遷移到雲端儲存方案。
\ No newline at end of file
diff --git a/cors.json b/cors.json
new file mode 100644
index 0000000..0f730b4
--- /dev/null
+++ b/cors.json
@@ -0,0 +1,12 @@
+[
+ {
+ "origin": [
+ "http://localhost:3000",
+ "http://localhost:3001",
+ "https://your-domain.com"
+ ],
+ "method": ["GET", "HEAD"],
+ "responseHeader": ["Content-Type"],
+ "maxAgeSeconds": 3600
+ }
+]
\ No newline at end of file
diff --git a/前端圖片URL處理機制詳解.md b/前端圖片URL處理機制詳解.md
new file mode 100644
index 0000000..300215a
--- /dev/null
+++ b/前端圖片URL處理機制詳解.md
@@ -0,0 +1,220 @@
+# 前端圖片 URL 處理機制詳解
+
+## 📋 概述
+
+本文檔詳細解釋 DramaLing 前端如何處理詞卡圖片 URL,以及為什麼不同格式的 URL 都能正常運作。
+
+## 🎯 核心工具:`flashcardUtils.ts`
+
+### 檔案位置
+```
+frontend/lib/utils/flashcardUtils.ts
+```
+
+### 檔案目的
+**統一管理詞卡相關的顯示和處理邏輯** - 避免在各個元件中重複寫相同的處理代碼
+
+---
+
+## 📝 核心函數詳細解析
+
+### 1. **圖片 URL 處理 - `getFlashcardImageUrl()`**
+
+**位置**: 第 77-99 行
+**用途**: 智能處理詞卡圖片 URL,支援多種格式和來源
+
+#### 處理邏輯流程:
+
+```typescript
+export const getFlashcardImageUrl = (flashcard: any): string | null => {
+ // 第一優先:檢查 primaryImageUrl
+ if (flashcard.primaryImageUrl) {
+ // 判斷是相對路徑還是完整 URL
+ if (flashcard.primaryImageUrl.startsWith('/')) {
+ // 相對路徑:拼接後端基礎 URL
+ return `${API_CONFIG.BASE_URL}${flashcard.primaryImageUrl}`
+ }
+ // 完整 URL:直接使用
+ return flashcard.primaryImageUrl
+ }
+
+ // 第二優先:檢查 exampleImages 陣列
+ if (flashcard.exampleImages && flashcard.exampleImages.length > 0) {
+ // 尋找標記為主要的圖片
+ const primaryImage = flashcard.exampleImages.find((img: any) => img.isPrimary)
+ if (primaryImage) {
+ const imageUrl = primaryImage.imageUrl
+ return imageUrl?.startsWith('/') ? `${API_CONFIG.BASE_URL}${imageUrl}` : imageUrl
+ }
+ // 沒有主要圖片,使用第一張
+ const firstImageUrl = flashcard.exampleImages[0].imageUrl
+ return firstImageUrl?.startsWith('/') ? `${API_CONFIG.BASE_URL}${firstImageUrl}` : firstImageUrl
+ }
+
+ // 都沒有圖片
+ return null
+}
+```
+
+#### 支援的 URL 格式:
+
+| 格式類型 | 範例 | 處理方式 |
+|---------|------|----------|
+| **Google Cloud Storage** | `https://storage.googleapis.com/dramaling-images/examples/file.png` | 直接使用完整 URL |
+| **本地服務** | `http://localhost:5008/images/examples/file.png` | 直接使用完整 URL |
+| **相對路徑** | `/images/examples/file.png` | 拼接為 `http://localhost:5008/images/examples/file.png` |
+
+---
+
+### 2. **其他工具函數**
+
+#### **詞性顯示 - `getPartOfSpeechDisplay()`**
+```typescript
+// 輸入:"noun" → 輸出:"n."
+// 輸入:"adjective" → 輸出:"adj."
+// 輸入:"preposition/adverb" → 輸出:"prep./adv." (支援複合詞性)
+```
+
+#### **CEFR 等級顏色 - `getCEFRColor()`**
+```typescript
+// A1 → "bg-green-100 text-green-700 border-green-200" (綠色)
+// B1 → "bg-yellow-100 text-yellow-700 border-yellow-200" (黃色)
+// C2 → "bg-purple-100 text-purple-700 border-purple-200" (紫色)
+```
+
+#### **熟練度處理**
+- **`getMasteryColor()`**: 根據數字返回顏色 (90+→綠色, <50→紅色)
+- **`getMasteryText()`**: 轉換為中文 (90+→"精通", <50→"學習中")
+
+#### **日期格式化**
+- **`formatNextReviewDate()`**: 複習時間 (過期→"需要複習", 明天→"明天")
+- **`formatCreatedDate()`**: 台灣日期格式 ("2024/1/15")
+
+#### **統計計算 - `calculateFlashcardStats()`**
+```typescript
+{
+ total: 總詞卡數,
+ mastered: 精通詞卡數 (熟練度 ≥ 80),
+ learning: 學習中詞卡數 (熟練度 40-79),
+ new: 新詞卡數 (熟練度 < 40),
+ favorites: 收藏詞卡數,
+ masteryPercentage: 精通百分比
+}
+```
+
+---
+
+## 🔍 實際運作流程
+
+### API 回應格式
+
+**目前狀態(修復後):**
+- `/api/flashcards`: 回傳完整 Google Cloud Storage URL
+- `/api/flashcards/{id}`: 回傳完整 Google Cloud Storage URL
+
+```json
+{
+ "primaryImageUrl": "https://storage.googleapis.com/dramaling-images/examples/b2bb23b8-16dd-44b2-bf64-34c468f2d362_e6498ba6-742b-473f-93b6-f4b58c3dd3e9.png"
+}
+```
+
+### 前端處理流程
+
+1. **接收 API 回應** → 取得 `primaryImageUrl`
+2. **呼叫 `getFlashcardImageUrl()`** → 檢查 URL 格式
+3. **格式判斷**:
+ - ✅ 完整 URL (`https://storage.googleapis.com/...`) → 直接使用
+ - ❌ 相對路徑 (`/images/...`) → 拼接後端域名(目前不會發生)
+4. **元件渲染** → `
`
+
+---
+
+## 🎯 設計優勢
+
+### **1. 兼容性設計**
+- 支援多種 URL 格式(相對路徑/完整 URL)
+- 可以無縫切換本地/雲端儲存
+- 向後兼容舊版 API
+
+### **2. 防禦性編程**
+- 多重備用方案(primaryImageUrl → exampleImages → null)
+- 自動處理路徑拼接邏輯
+- 避免因 API 格式變更導致圖片顯示失敗
+
+### **3. 集中化管理**
+- 所有圖片 URL 處理邏輯集中在一個函數
+- 修改邏輯時只需要改一個地方
+- 多個元件可以重用相同邏輯
+
+### **4. 模組化架構**
+- 每個功能都有專門的工具函數
+- 易於測試和維護
+- 符合 DRY (Don't Repeat Yourself) 原則
+
+---
+
+## 🔧 使用範例
+
+### 在元件中使用
+
+```typescript
+// FlashcardCard.tsx
+import { getFlashcardImageUrl, getCEFRColor, getPartOfSpeechDisplay } from '@/lib/utils/flashcardUtils'
+
+// 取得圖片 URL
+const imageUrl = getFlashcardImageUrl(flashcard)
+// 結果:https://storage.googleapis.com/dramaling-images/examples/file.png
+
+// 取得 CEFR 顏色
+const cefrClasses = getCEFRColor(flashcard.cefr)
+// 結果:"bg-blue-100 text-blue-700 border-blue-200"
+
+// 取得詞性簡寫
+const partOfSpeech = getPartOfSpeechDisplay(flashcard.partOfSpeech)
+// 結果:"n."
+
+// 在 JSX 中使用
+
+{flashcard.cefr}
+{partOfSpeech}
+```
+
+---
+
+## 📊 問題解答
+
+### Q: 為什麼不同格式的 URL 都能正常運作?
+
+**A**: 前端設計了智能兼容性處理:
+
+1. **完整 URL** → 直接使用(目前的情況)
+2. **相對路徝** → 自動拼接後端域名(向後兼容)
+3. **多重備用** → primaryImageUrl 失敗時使用 exampleImages
+
+### Q: 這樣的設計有什麼好處?
+
+**A**:
+- ✅ **彈性切換**:可以在本地開發和雲端部署間切換
+- ✅ **向後兼容**:支援舊版 API 格式
+- ✅ **錯誤處理**:多重備用方案確保圖片顯示
+- ✅ **維護性**:集中化管理,易於修改
+
+### Q: 目前系統的實際運作狀況?
+
+**A**:
+- 後端統一回傳完整的 Google Cloud Storage URLs
+- 前端接收到完整 URL,直接使用(不進入相對路徑處理邏輯)
+- 圖片正常顯示,系統運作正常
+
+---
+
+## 📈 總結
+
+`flashcardUtils.ts` 是一個設計良好的工具函數庫,實現了:
+
+- **統一化**:所有詞卡相關的顯示邏輯集中管理
+- **兼容性**:支援多種 URL 格式和資料來源
+- **可維護性**:模組化設計,易於擴展和修改
+- **可靠性**:防禦性編程,確保系統穩定運作
+
+這種設計確保了前端系統的健壯性和可維護性,是現代前端架構的最佳實務範例。
\ No newline at end of file