feat:實作課程圖片上傳 — 封面 + 相簿管理

後端:
- Migration:diving_offers 新增 cover_image 欄位、新增 course_images 表(含索引)
- CourseImage Model(CREATED_AT、url accessor)
- DivingOffer:cover_image_url accessor、hasMany courseImages、static::deleting() 孤兒清理
- CourseImageController:封面上傳/刪除、相簿上傳(max 3)/刪除,統一 mimes+size 驗證
- DivingOfferController:index/show 回傳加入 cover_image_url 與 images 陣列
- 修正 APP_URL 加入 port(:8080),Storage::url() 才能產生正確圖片連結

前端:
- courseImageApi.js:uploadCover/deleteCover/uploadImage/deleteImage
- CourseCard:有封面顯示 <img>,無封面顯示漸層佔位
- CourseDetailView:封面大圖 + 相簿縮圖橫列(點擊開新分頁)
- OfferFormView(編輯模式):封面預覽/更換/刪除、相簿縮圖管理(達 3 張隱藏上傳按鈕)

基礎設施:
- docker-entrypoint.sh:加入 storage:link --force
- docker-compose.yml:移除 storage-data named volume(改用 bind mount,避免 Nginx 讀不到圖片)

測試:
- CourseImageTest.php:14 個 Feature Test 全部 PASS(Storage::fake)
  涵蓋:上傳成功/格式驗證/大小驗證/所有權、刪除/無封面不報錯、
        相簿上限/sort_order 遞增、孤兒清理

OpenSpec:
- course-images change 歸檔至 archive/2026-05-12-course-images
- 新增 specs/course-image-upload 主規格(含 bind mount 持久化說明)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 03:54:45 +08:00
parent 81a9f84b26
commit 4baa4cb52b
20 changed files with 1048 additions and 11 deletions
@@ -0,0 +1,41 @@
## Why
目前課程頁面以 🤿 emoji 作為佔位,缺乏視覺吸引力。課程圖片是潛水平台的核心體驗,直接影響會員瀏覽意願與教練品牌形象。
## What Changes
- 新增 `diving_offers.cover_image` 欄位:課程封面,顯示於課程卡與詳情頁頂部
- 新增 `course_images` 資料表:相簿最多 3 張,顯示於課程詳情頁
- 新增 Provider API:上傳/刪除封面、上傳/刪除相簿圖片
- 更新前端:CourseCard 顯示封面、CourseDetailView 顯示封面 + 相簿、OfferFormView 加入圖片管理 UI
- Docker volume 掛載 `storage/app/public`,圖片跨 build 持久化
- `php artisan storage:link` 整合至 entrypoint,確保 symlink 正確
## Capabilities
### New Capabilities
- `course-image-upload`:Provider 上傳課程封面與相簿圖片(本地 public disk、max 2MB、支援 jpg/jpeg/png/webp)、相簿上限 3 張、刪除圖片同步移除實體檔案
### Modified Capabilities
- `coach-offers-api`:新增圖片上傳/刪除端點,`DivingOffer` 回應加入 `cover_image_url``images`
## Impact
**後端**
- Migration`diving_offers``cover_image`nullable string
- Migration:新增 `course_images`id、diving_offer_id、image_path、sort_order
- Model`CourseImage``DivingOffer``hasMany CourseImage``cover_image_url` accessor
- Controller`CourseImageController`(上傳封面、刪除封面、上傳相簿、刪除相簿)
- Route`/provider/offers/{id}/cover`POST/DELETE)、`/provider/offers/{id}/images`POST)、`/provider/images/{id}`DELETE
**前端**
- `frontend/src/api/courseImageApi.js`
- `CourseCard.vue`:有封面顯示圖片,無封面顯示漸層佔位
- `CourseDetailView.vue`:頂部大圖(封面)+ 相簿縮圖列
- `OfferFormView.vue`(或新建 `OfferImageManager.vue`):封面上傳預覽、相簿管理
**Docker**
- `docker-compose.yml` 新增 named volume `storage-data` 掛載 `/var/www/storage/app/public`
- `docker-entrypoint.sh` 加入 `php artisan storage:link --force`