US-103: 商品照片管理
User Story
作為 花店管理者 我想要 能夠上傳、管理和排序商品照片 以便 為商品提供視覺展示,提升客戶購買意願
驗收標準 (Acceptance Criteria)
Scenario 1: 上傳商品主圖
- Given 我是已登入的管理者
- And 我在「新增商品」或「編輯商品」頁面
- When 我點擊主圖上傳區域
- And 我選擇一張 JPG 格式、大小 2MB 的照片
- Then 系統應顯示上傳進度
- And 上傳完成後應顯示照片預覽
- And 系統應自動生成縮圖版本
- And 系統應自動轉換為 WebP 格式
Scenario 2: 拖曳上傳主圖
- Given 我是已登入的管理者
- And 我在商品表單頁面
- When 我將一張照片檔案拖曳到主圖上傳區域
- Then 拖曳區域應顯示視覺提示(邊框變色)
- And 放開後應開始上傳
- And 上傳完成後應顯示照片預覽
Scenario 3: 上傳多張副圖
- Given 我是已登入的管理者
- And 我在商品表單頁面
- And 商品已有主圖
- When 我點擊「新增副圖」按鈕
- And 我一次選擇 3 張照片
- Then 系統應顯示每張照片的上傳進度
- And 上傳完成後應在副圖區域顯示所有照片
- And 照片應按上傳順序排列
Scenario 4: 拖曳排序副圖
- Given 我是已登入的管理者
- And 商品已有 4 張副圖
- When 我拖曳第 4 張照片到第 1 張的位置
- Then 照片順序應更新為:原第4張、原第1張、原第2張、原第3張
- And 排序變更應即時儲存
Scenario 5: 設定副圖為主圖
- Given 我是已登入的管理者
- And 商品已有 1 張主圖和 3 張副圖
- When 我點擊第 2 張副圖的「設為主圖」按鈕
- Then 原本的主圖應移動到副圖區域的最前面
- And 第 2 張副圖應成為新的主圖
Scenario 6: 刪除副圖
- Given 我是已登入的管理者
- And 商品已有 1 張主圖和 3 張副圖
- When 我點擊第 1 張副圖的「刪除」按鈕
- And 系統要求我確認
- And 我點擊「確認刪除」
- Then 該副圖應被移除
- And 剩餘副圖應重新排序
- And 雲端儲存的照片檔案也應被刪除
Scenario 7: 更換主圖
- Given 我是已登入的管理者
- And 商品已有主圖
- When 我點擊主圖的「更換」按鈕
- And 我選擇一張新照片
- Then 新照片應替換原本的主圖
- And 原本的主圖應被刪除
Scenario 8: 照片大小超過限制(錯誤場景)
- Given 我是已登入的管理者
- And 我在商品表單頁面
- When 我嘗試上傳一張 15MB 的照片
- Then 系統應顯示錯誤訊息「照片大小不能超過 10MB」
- And 照片不應被上傳
Scenario 9: 照片格式不支援(錯誤場景)
- Given 我是已登入的管理者
- And 我在商品表單頁面
- When 我嘗試上傳一張 GIF 格式的檔案
- Then 系統應顯示錯誤訊息「僅支援 JPEG、PNG、WebP 格式」
- And 檔案不應被上傳
Scenario 10: 副圖數量超過限制(錯誤場景)
- Given 我是已登入的管理者
- And 商品已有 5 張副圖(已達上限)
- When 我嘗試新增第 6 張副圖
- Then 系統應顯示錯誤訊息「副圖最多 5 張,請先刪除現有照片」
- And 新照片不應被上傳
Scenario 11: 上傳過程中網路中斷(錯誤場景)
- Given 我是已登入的管理者
- And 我正在上傳一張照片
- When 上傳過程中網路連線中斷
- Then 系統應顯示錯誤訊息「上傳失敗,請檢查網路連線後重試」
- And 應顯示「重試」按鈕
Scenario 12: 預覽商品照片
- Given 我是已登入的員工
- And 我在商品詳情頁
- And 商品有 1 張主圖和 3 張副圖
- When 我點擊主圖
- Then 系統應開啟照片預覽 Modal(Lightbox)
- And 應顯示高解析度版本
- And 可左右滑動/點擊切換照片
- And 點擊背景或關閉按鈕可關閉預覽
業務規則 (Business Rules)
-
照片數量限制
- 主圖: 必須有且只有 1 張
- 副圖: 最少 0 張,最多 5 張
- 總計: 最多 6 張照片/商品
-
照片格式規範
- 支援格式: JPEG (.jpg, .jpeg)、PNG (.png)、WebP (.webp)
- 不支援: GIF、BMP、TIFF、RAW 等格式
- 上傳後自動轉換為 WebP 格式(保留原檔備份)
-
照片大小規範
- 單張最大: 10MB
- 建議尺寸: 800x800px 以上
- 最小尺寸: 200x200px
- 最大尺寸: 4096x4096px
-
自動處理
- 縮圖生成: 自動生成 200x200px 縮圖(用於列表顯示)
- 壓縮版本: 自動生成 Web 顯示用壓縮版本(品質 85%)
- 格式轉換: 自動轉換為 WebP 格式(減少 25-35% 體積)
- EXIF 清除: 清除照片中的隱私資訊(GPS 等)
-
儲存路徑規則
/{bucket}/tenant-{tenantId}/products/{productId}/
├── main.webp # 主圖(壓縮版)
├── main_original.jpg # 主圖(原檔)
├── main_thumb.webp # 主圖(縮圖)
├── gallery_1.webp # 副圖 1(壓縮版)
├── gallery_1_original.jpg # 副圖 1(原檔)
├── gallery_1_thumb.webp # 副圖 1(縮圖)
└── ... -
排序規則
- 副圖順序由用戶自訂(拖曳排序)
- 排序變更即時儲存
- 順序用於商品詳情頁展示
-
刪除規則
- 刪除副圖時同時刪除雲端儲存的所有版本(原檔、壓縮版、縮圖)
- 主圖不可直接刪除,只能更換
- 商品刪除時一併刪除所有照片
-
權限規則
ROLE_MANAGER以上可上傳/刪除照片ROLE_STAFF可查看照片但無法修改- 所有用戶可預覽照片(Lightbox)
-
多租戶隔離
- 照片 URL 包含
tenantId - 非授權訪問其他租戶照片返回 403
- CDN 快取鍵包含
tenantId
- 照片 URL 包含
UI/UX 需求 (UI/UX Requirements)
主圖上傳區域
┌─────────────────────────────────┐
│ │
│ [點擊上傳或拖曳照片到此] │
│ │
│ 支援 JPG、PNG、WebP │
│ 最大 10MB │
│ │
└─────────────────────────────────┘
上傳後狀態:
┌─────────────────────────────────┐
│ │
│ [照片預覽] │
│ │
├─────────────────────────────────┤
│ [更換] [預覽] │
└─────────────────────────────────┘
副圖管理區域
副圖(最多 5 張)
┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐
│ 1 │ │ 2 │ │ 3 │ │ + │ │ │ │ │
│[圖]│ │[圖]│ │[圖]│ │新增│ │ │ │ │
│ × │ │ × │ │ × │ │ │ │ │ │ │
└────┘ └────┘ └────┘ └────┘ └────┘ └────┘
↔ 可拖曳排序
每張副圖的操作:
- Hover 顯示操作按鈕
- 「×」刪除按鈕(右上角)
- 「⭐」設為主圖按鈕
- 拖曳手把圖示
上傳進度顯示
┌─────────────────────────────────┐
│ filename.jpg │
│ ████████████░░░░░░░░ 65% │
│ 上傳中... │
└─────────────────────────────────┘
Lightbox 預覽
┌───────────────────────────────────────────────┐
│ [×] │
│ │
│ [<] [高解析度照片] [>] │
│ │
│ │
│ ○ ○ ● ○ ○ (分頁指示器) │
└───────────────────────────────────────────────┘
互動行為
-
拖曳上傳:
- 拖曳檔案到區域時,邊框變為藍色虛線
- 區域文字變為「放開以上傳」
- 不支援的檔案類型時,邊框變為紅色
-
多檔上傳:
- 同時上傳多張時,顯示每張的進度
- 任一張失敗不影響其他張
- 全部完成後顯示總結訊息
-
拖曳排序:
- 長按/點擊拖曳手把開始拖曳
- 拖曳中顯示半透明預覽
- 放置位置顯示藍色插入線
- 放開後即時更新順序
-
預覽 Lightbox:
- 支援左右方向鍵切換
- 支援 Esc 鍵關閉
- 支援觸控滑動(手機)
- 縮放功能(雙擊/捏合)
錯誤狀態
- 檔案過大: 紅色提示框「照片大小不能超過 10MB」
- 格式錯誤: 紅色提示框「僅支援 JPEG、PNG、WebP 格式」
- 上傳失敗: 顯示錯誤圖示 + 「重試」按鈕
- 數量超限: 紅色提示框「副圖最多 5 張」
響應式設計
- 桌面: 6 欄網格顯示副圖
- 平板: 4 欄網格顯示副圖
- 手機: 3 欄網格顯示副圖,Lightbox 全螢幕
技術規格 (Technical Specifications)
API 端點
1. 上傳照片
- 端點:
POST /api/v1/products/upload-image - 權限要求:
ROLE_MANAGER或更高 - Content-Type:
multipart/form-data
請求:
POST /api/v1/products/upload-image
Content-Type: multipart/form-data
file: (binary)
productId: prod-abc123 (optional, for existing product)
imageType: main | gallery
響應 Payload:
{
"id": "img-xyz789",
"url": "https://cdn.example.com/tenant-001/products/prod-abc123/main.webp",
"thumbnailUrl": "https://cdn.example.com/tenant-001/products/prod-abc123/main_thumb.webp",
"originalUrl": "https://cdn.example.com/tenant-001/products/prod-abc123/main_original.jpg",
"width": 1200,
"height": 1200,
"size": 245678,
"format": "webp"
}
2. 刪除照片
- 端點:
DELETE /api/v1/products/{productId}/images/{imageId} - 權限要求:
ROLE_MANAGER或更高
響應: 204 No Content
3. 更新照片順序
- 端點:
PUT /api/v1/products/{productId}/images/order - 權限要求:
ROLE_MANAGER或更高
請求 Payload:
{
"galleryImageIds": ["img-001", "img-003", "img-002"] // 新順序
}
響應: 200 OK
4. 設定主圖
- 端點:
PUT /api/v1/products/{productId}/main-image - 權限要求:
ROLE_MANAGER或更高
請求 Payload:
{
"imageId": "img-003" // 將此副圖設為主圖
}
響應 Payload:
{
"mainImageUrl": "https://cdn.example.com/.../main.webp",
"galleryImageUrls": ["https://..."] // 更新後的副圖列表
}
前端驗證
// 檔案驗證
const validateImageFile = (file: File): ValidationResult => {
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MIN_DIMENSION = 200;
const MAX_DIMENSION = 4096;
if (file.size > MAX_SIZE) {
return { valid: false, error: '照片大小不能超過 10MB' };
}
if (!ALLOWED_TYPES.includes(file.type)) {
return { valid: false, error: '僅支援 JPEG、PNG、WebP 格式' };
}
// 需要讀取圖片後驗證尺寸
return { valid: true };
};
// 數量驗證
const validateGalleryCount = (currentCount: number, addCount: number): ValidationResult => {
const MAX_GALLERY = 5;
if (currentCount + addCount > MAX_GALLERY) {
return { valid: false, error: `副圖最多 ${MAX_GALLERY} 張,請先刪除現有照片` };
}
return { valid: true };
};
元件架構
src/applets/product-applet/
├── components/
│ ├── ProductImageUploader.tsx # 照片上傳主組件
│ ├── MainImageUploader.tsx # 主圖上傳區域
│ ├── GalleryImageUploader.tsx # 副圖管理區域
│ ├── ImageDropzone.tsx # 拖曳上傳區域
│ ├── ImagePreview.tsx # 單張照片預覽
│ ├── ImageUploadProgress.tsx # 上傳進度指示器
│ ├── ImageLightbox.tsx # Lightbox 預覽
│ └── SortableImageGrid.tsx # 可排序的照片網格
├── hooks/
│ ├── useImageUpload.ts # 照片上傳邏輯
│ ├── useImageDragDrop.ts # 拖曳上傳邏輯
│ ├── useImageSorting.ts # 照片排序邏輯
│ └── useLightbox.ts # Lightbox 控制邏輯
└── utils/
├── imageValidation.ts # 照片驗證工具
└── imageCompression.ts # 前端壓縮(可選)
依賴套件
{
"dependencies": {
"react-dropzone": "^14.x", // 拖曳上傳
"@dnd-kit/core": "^6.x", // 拖曳排序
"@dnd-kit/sortable": "^7.x", // 可排序列表
"yet-another-react-lightbox": "^3.x" // Lightbox
}
}
測試需求 (Test Requirements)
單元測試
- 檔案驗證邏輯(大小、格式、尺寸)
- 照片數量驗證邏輯
- 排序更新邏輯
- 主圖切換邏輯
整合測試
- 照片上傳 API 整合
- 照片刪除 API 整合
- 排序更新 API 整合
- 主圖設定 API 整合
- CDN URL 生成正確性
E2E 測試
- Scenario 1: 上傳商品主圖
- Scenario 2: 拖曳上傳主圖
- Scenario 3: 上傳多張副圖
- Scenario 4: 拖曳排序副圖
- Scenario 5: 設定副圖為主圖
- Scenario 6: 刪除副圖
- Scenario 8: 照片大小超過限制
- Scenario 9: 照片格式不支援
- Scenario 10: 副圖數量超過限制
- Scenario 12: 預覽商品照片
驗收檢查清單 (Acceptance Checklist)
- 主圖上傳功能正常
- 拖曳上傳功能正常
- 多檔上傳功能正常
- 副圖排序功能正常
- 設定副圖為主圖功能正常
- 刪除副圖功能正常
- 更換主圖功能正常
- 檔案大小驗證正常
- 檔案格式驗證正常
- 副圖數量限制正常
- 上傳進度顯示正常
- 上傳失敗時顯示錯誤訊息
- Lightbox 預覽功能正常
- 響應式設計正常(桌面/平板/手機)
- 多租戶隔離正常
Story Points
估算: 5 points
理由:
- 照片上傳邏輯較複雜(支援拖曳、多檔、進度)
- 拖曳排序需要整合 DnD 套件
- Lightbox 預覽需要整合第三方套件
- 需要處理各種錯誤情況
- 後端需要整合雲端儲存
相關文檔
- Epic 1: 產品與作品集管理
- US-101: 商品基本資料管理
- US-102: 商品列表與搜尋
- Epic 5: 數位資產管理 (待創建)
最後更新: 2025-12-03 撰寫者: AI Assistant