# Payload CMS 修改計劃 - Story 1.2 詳細規劃

**建立日期：** 2025-01-30
**適用範圍：** Story 1.2 - Payload CMS Collections Definition
**目標：** 完善所有 Collections 以符合 PRD 需求

---

## 📊 執行摘要

### 當前狀態
| Collection | 完成度 | 缺失項目 | 優先級 | 預估時間 |
|-----------|--------|---------|--------|---------|
| Portfolio | ❌ 0% | 整個 Collection | 🔴 P0 | 1 小時 |
| Categories | ⚠️ 40% | 4 個欄位 | 🔴 P0 | 30 分鐘 |
| Posts | ⚠️ 60% | 4 個欄位 | 🟡 P1 | 30 分鐘 |
| Users | ⚠️ 70% | 1 個欄位 | 🟡 P1 | 30 分鐘 |
| Access Control | ⚠️ 50% | 2 個函數 | 🟡 P1 | 1 小時 |

**總預估時間：** 4 小時

---

## 🎯 實作策略

### 階段劃分

**Phase 1: Critical Blockers（必須優先）**
- 目標：解除 Story 1.9 (Blog) 和 1.10 (Portfolio) 的阻礙
- 時間：1.5 小時
- 內容：
  - Task 1.2.1: 創建 Portfolio Collection
  - Task 1.2.2: 完善 Categories Collection

**Phase 2: Content Enhancement（次要優先）**
- 目標：完善內容管理功能
- 時間：1 小時
- 內容：
  - Task 1.2.3: 完善 Posts Collection
  - Task 1.2.4: 完善 Users Collection

**Phase 3: Security & Access（最後）**
- 目標：實現角色權限系統
- 時間：1.5 小時
- 內容：
  - Task 1.2.5: 創建 Access Control 函數
  - Task 1.2.6: 應用 Access Control
  - Task 1.2.7: 驗證和測試

---

## 📋 詳細任務定義

### Task 1.2.1: 創建 Portfolio Collection

**User Story:**
```gherkin
As a Developer,
I want to create a Portfolio collection in Payload CMS,
So that website projects can be stored and managed.

Acceptance Criteria:
- Portfolio collection exists with slug 'portfolio'
- Has 7 required fields: title, slug, url, image, description, websiteType, tags
- Image field uploads to R2 storage
- Access control: authenticated users can read, admins can edit
- Collection appears in admin sidebar
```

**Definition of Done:**
- [ ] Collection config file created at `apps/backend/src/collections/Portfolio/index.ts`
- [ ] All 7 fields defined with correct types
- [ ] Admin UI labels configured (Chinese)
- [ ] Access control functions applied
- [ ] Registered in `payload.config.ts`
- [ ] Types regenerated successfully (`pnpm generate:types`)
- [ ] Verified in admin panel

**檔案結構：**
```
apps/backend/src/collections/Portfolio/
├── index.ts           # Main collection config
└── hooks/             # Optional hooks (if needed)
```

**完整欄位定義：**

```typescript
// apps/backend/src/collections/Portfolio/index.ts
import type { CollectionConfig } from 'payload'
import { authenticated } from '../../access/authenticated'
import { anyone } from '../../access/anyone'
import { slugField } from '@/fields/slug'

export const Portfolio: CollectionConfig = {
  slug: 'portfolio',
  access: {
    create: authenticated,
    read: anyone,
    update: authenticated,
    delete: authenticated,
  },
  admin: {
    useAsTitle: 'title',
    defaultColumns: ['title', 'websiteType', 'updatedAt', 'createdAt'],
  },
  fields: [
    {
      name: 'title',
      type: 'text',
      required: true,
      label: '專案標題',
    },
    {
      name: 'url',
      type: 'text',
      label: '外部連結',
      admin: {
        description: '前往此專案網站的連結（可選）',
      },
    },
    {
      name: 'image',
      type: 'upload',
      relationTo: 'media',
      required: true,
      label: '專案圖片',
    },
    {
      name: 'description',
      type: 'textarea',
      label: '專案描述',
      admin: {
        description: '簡短描述此專案的特點',
      },
    },
    {
      name: 'websiteType',
      type: 'select',
      required: true,
      label: '網站類型',
      options: [
        { label: '一頁式銷售', value: 'landing-page' },
        { label: '客戶預約', value: 'booking' },
        { label: '企業官網', value: 'corporate' },
        { label: '電商網站', value: 'ecommerce' },
        { label: '其他', value: 'other' },
      ],
    },
    {
      name: 'tags',
      type: 'array',
      label: '標籤',
      fields: [
        {
          name: 'tag',
          type: 'text',
        },
      ],
    },
    ...slugField(),
  ],
  hooks: {
    beforeChange: [
      ({ data, operation }) => {
        // Auto-generate slug from title if creating
        if (operation === 'create' && !data.slug) {
          return {
            ...data,
            slug: data.title
              .toLowerCase()
              .replace(/[^\w\s-]/g, '')
              .replace(/\s+/g, '-'),
          }
        }
        return data
      },
    ],
  },
}
```

**更新 payload.config.ts：**
```typescript
// 在 apps/backend/src/payload.config.ts
import { Portfolio } from './collections/Portfolio'

// 在 collections array 中添加
collections: [Pages, Posts, Media, Categories, Users, Portfolio],
```

**預估時間：** 1 小時

---

### Task 1.2.2: 完善 Categories Collection

**User Story:**
```gherkin
As a Developer,
I want to add missing fields to Categories collection,
So that blog categories have complete metadata for theming.

Acceptance Criteria:
- nameEn field added (English name)
- order field added (sorting, default: 0)
- textColor field added (with color picker)
- backgroundColor field added (with color picker)
- All fields appear in admin UI
- Fields are editable and save correctly
```

**Definition of Done:**
- [ ] 4 new fields added to Categories collection
- [ ] Admin UI configured with proper labels (Chinese)
- [ ] Color picker enabled for color fields
- [ ] Types regenerated successfully
- [ ] Tested in admin panel

**修改檔案：** `apps/backend/src/collections/Categories.ts`

**新增欄位定義：**

```typescript
export const Categories: CollectionConfig = {
  slug: 'categories',
  access: {
    create: authenticated,
    delete: authenticated,
    read: anyone,
    update: authenticated,
  },
  admin: {
    useAsTitle: 'title',
    defaultColumns: ['title', 'order', 'updatedAt'],
  },
  fields: [
    {
      name: 'title',
      type: 'text',
      required: true,
      label: '分類名稱（中文）',
    },
    {
      name: 'nameEn',         // ✨ 新增
      type: 'text',
      label: '英文名稱',
      admin: {
        description: '用於 URL 或國際化',
      },
    },
    {
      name: 'order',          // ✨ 新增
      type: 'number',
      label: '排序順序',
      defaultValue: 0,
      admin: {
        position: 'sidebar',
        description: '數字越小越靠前',
      },
    },
    {
      name: 'textColor',      // ✨ 新增
      type: 'text',
      label: '文字顏色',
      defaultValue: '#000000',
      admin: {
        description: '十六進制顏色碼，例如 #000000',
      },
    },
    {
      name: 'backgroundColor', // ✨ 新增
      type: 'text',
      label: '背景顏色',
      defaultValue: '#ffffff',
      admin: {
        description: '十六進制顏色碼，例如 #ffffff',
      },
    },
    ...slugField(),
  ],
}
```

**預估時間：** 30 分鐘

---

### Task 1.2.3: 完善 Posts Collection

**User Story:**
```gherkin
As a Developer,
I want to add missing fields to Posts collection,
So that blog posts have complete metadata for display and SEO.

Acceptance Criteria:
- excerpt field added (textarea, 200 char limit)
- ogImage field added (upload to media, separate from heroImage)
- showInFooter field added (checkbox, default: false)
- status field added (select: draft, review, published)
- All fields appear in post editor
- Fields save and load correctly
```

**Definition of Done:**
- [ ] 4 new fields added to Posts collection
- [ ] Admin UI configured with proper labels
- [ ] excerpt field has character limit (200)
- [ ] ogImage has image preview
- [ ] Types regenerated successfully
- [ ] Tested creating/editing a post

**修改檔案：** `apps/backend/src/collections/Posts/index.ts`

**新增欄位定義：**

```typescript
// 在 fields array 的 Content tab 中添加
{
  name: 'excerpt',        // ✨ 新增
  type: 'text',
  label: '文章摘要',
  admin: {
    description: '顯示在文章列表頁，建議 150-200 字',
    multiline: true,
  },
  maxLength: 200,
}

// 在 Content tab 中添加（heroImage 之後）
{
  name: 'ogImage',         // ✨ 新增
  type: 'upload',
  relationTo: 'media',
  label: '社群分享圖片',
  admin: {
    description: 'Facebook/LINE 分享時顯示的預覽圖，建議 1200x630px',
  },
}

// 在 Meta tab (sidebar) 中添加
{
  name: 'showInFooter',   // ✨ 新增
  type: 'checkbox',
  label: '顯示在頁腳',
  defaultValue: false,
  admin: {
    position: 'sidebar',
  },
}

// 在 sidebar 中添加（publishedAt 附近）
{
  name: 'status',         // ✨ 新增
  type: 'select',
  label: '文章狀態',
  defaultValue: 'draft',
  options: [
    { label: '草稿', value: 'draft' },
    { label: '審核中', value: 'review' },
    { label: '已發布', value: 'published' },
  ],
  admin: {
    position: 'sidebar',
  },
}
```

**預估時間：** 30 分鐘

---

### Task 1.2.4: 完善 Users Collection

**User Story:**
```gherkin
As an Admin,
I want to assign roles to users,
So that I can control who has access to different features.

Acceptance Criteria:
- role field added to Users collection (admin, editor)
- Default value is 'editor'
- Field appears in user editor
- Existing users can be updated
```

**Definition of Done:**
- [ ] role field added to Users collection
- [ ] Default value: 'editor'
- [ ] Admin UI configured with Chinese labels
- [ ] Types regenerated successfully
- [ ] Tested with both admin and editor users

**修改檔案：** `apps/backend/src/collections/Users/index.ts`

**新增欄位定義：**

```typescript
export const Users: CollectionConfig = {
  slug: 'users',
  access: {
    admin: authenticated,
    create: authenticated,
    delete: authenticated,
    read: authenticated,
    update: authenticated,
  },
  admin: {
    defaultColumns: ['name', 'email', 'role'],
    useAsTitle: 'name',
  },
  auth: true,
  fields: [
    {
      name: 'name',
      type: 'text',
      label: '姓名',
    },
    {
      name: 'role',           // ✨ 新增
      type: 'select',
      label: '角色',
      defaultValue: 'editor',
      required: true,
      options: [
        {
          label: '管理員',
          value: 'admin',
        },
        {
          label: '編輯者',
          value: 'editor',
        },
      ],
      admin: {
        position: 'sidebar',
      },
    },
  ],
  timestamps: true,
}
```

**預估時間：** 30 分鐘

---

### Task 1.2.5: 創建 Access Control 函數

**User Story:**
```gherkin
As an Admin,
I want different access levels for admins and editors,
So that editors can only manage content, not system settings.

Acceptance Criteria:
- adminOnly() access function created
- adminOrEditor() access function created
- Functions check user.role field
- Functions return boolean
```

**Definition of Done:**
- [ ] adminOnly.ts file created
- [ ] adminOrEditor.ts file created
- [ ] Functions check user.role correctly
- [ ] TypeScript types correct

**創建檔案 1：** `apps/backend/src/access/adminOnly.ts`

```typescript
import type { Access } from 'payload'

/**
 * 僅允許 Admin 角色訪問
 *
 * 用例：
 * - Users collection (敏感操作)
 * - Globals (Header/Footer)
 * - System settings
 */
export const adminOnly: Access = ({ req: { user } }) => {
  return user?.role === 'admin'
}
```

**創建檔案 2：** `apps/backend/src/access/adminOrEditor.ts`

```typescript
import type { Access } from 'payload'

/**
 * 允許 Admin 或 Editor 角色訪問
 *
 * 用例：
 * - Posts/Pages collection (內容管理)
 * - Categories collection (內容分類)
 * - Portfolio collection (作品管理)
 */
export const adminOrEditor: Access = ({ req: { user } }) => {
  if (!user) return false
  return user?.role === 'admin' || user?.role === 'editor'
}
```

**預估時間：** 30 分鐘

---

### Task 1.2.6: 應用 Access Control

**User Story:**
```gherkin
As an Admin,
I want access control applied to all collections,
So that role-based permissions work correctly.

Acceptance Criteria:
- Users collection: delete restricted to admins
- Posts/Pages: create/update/delete restricted to adminOrEditor
- Categories/Portfolio: create/update/delete restricted to adminOrEditor
- Globals: update restricted to admins
- All collections read access preserved
```

**Definition of Done:**
- [ ] Users collection updated with adminOnly
- [ ] Posts collection updated with adminOrEditor
- [ ] Pages collection updated with adminOrEditor
- [ ] Categories collection updated with adminOrEditor
- [ ] Portfolio collection updated with adminOrEditor
- [ ] Header global updated with adminOnly
- [ ] Footer global updated with adminOnly
- [ ] Types regenerated
- [ ] Tested with both admin and editor users

**修改檔案 1：** `apps/backend/src/collections/Users/index.ts`

```typescript
import { adminOnly } from '../../access/adminOnly'

export const Users: CollectionConfig = {
  slug: 'users',
  access: {
    admin: adminOnly,        // ❌ 改為 adminOnly
    create: adminOnly,       // ❌ 改為 adminOnly
    delete: adminOnly,       // ❌ 改為 adminOnly
    read: authenticated,     // ✅ 保持
    update: adminOnly,       // ❌ 改為 adminOnly
  },
  // ...
}
```

**修改檔案 2：** `apps/backend/src/collections/Posts/index.ts`

```typescript
import { adminOrEditor } from '../../access/adminOrEditor'

export const Posts: CollectionConfig = {
  slug: 'posts',
  access: {
    create: adminOrEditor,   // ❌ 改為 adminOrEditor
    delete: adminOrEditor,   // ❌ 改為 adminOrEditor
    read: authenticatedOrPublished, // ✅ 保持
    update: adminOrEditor,   // ❌ 改為 adminOrEditor
  },
  // ...
}
```

**修改檔案 3：** `apps/backend/src/collections/Pages/index.ts`

```typescript
import { adminOrEditor } from '../../access/adminOrEditor'

export const Pages: CollectionConfig = {
  slug: 'pages',
  access: {
    create: adminOrEditor,   // ❌ 改為 adminOrEditor
    delete: adminOrEditor,   // ❌ 改為 adminOrEditor
    read: authenticatedOrPublished, // ✅ 保持
    update: adminOrEditor,   // ❌ 改為 adminOrEditor
  },
  // ...
}
```

**修改檔案 4：** `apps/backend/src/collections/Categories.ts`

```typescript
import { adminOrEditor } from '../access/adminOrEditor'

export const Categories: CollectionConfig = {
  slug: 'categories',
  access: {
    create: adminOrEditor,   // ❌ 改為 adminOrEditor
    delete: adminOrEditor,   // ❌ 改為 adminOrEditor
    read: anyone,            // ✅ 保持（公開可讀）
    update: adminOrEditor,   // ❌ 改為 adminOrEditor
  },
  // ...
}
```

**修改檔案 5：** `apps/backend/src/collections/Portfolio/index.ts`

```typescript
import { adminOrEditor } from '../../access/adminOrEditor'
import { anyone } from '../../access/anyone'

export const Portfolio: CollectionConfig = {
  slug: 'portfolio',
  access: {
    create: adminOrEditor,   // ❌ 使用 adminOrEditor
    read: anyone,            // ✅ 公開可讀
    update: adminOrEditor,   // ❌ 使用 adminOrEditor
    delete: adminOrEditor,   // ❌ 使用 adminOrEditor
  },
  // ...
}
```

**修改檔案 6：** `apps/backend/src/Header/config.ts`

```typescript
import { adminOnly } from '../access/adminOnly'

export const Header: GlobalConfig = {
  slug: 'header',
  access: {
    read: anyone,            // ✅ 公開可讀
    update: adminOnly,       // ❌ 僅 Admin 可編輯
  },
  // ...
}
```

**修改檔案 7：** `apps/backend/src/Footer/config.ts`

```typescript
import { adminOnly } from '../access/adminOnly'

export const Footer: GlobalConfig = {
  slug: 'footer',
  access: {
    read: anyone,            // ✅ 公開可讀
    update: adminOnly,       // ❌ 僅 Admin 可編輯
  },
  // ...
}
```

**預估時間：** 30 分鐘

---

### Task 1.2.7: 驗證和測試

**User Story:**
```gherkin
As a QA Engineer,
I want to verify all collections work correctly,
So that content management is functional.

Acceptance Criteria:
- All collections appear in admin sidebar
- All fields are editable in admin UI
- Role-based access control works correctly
- Media uploads to R2 successfully
- Rich text editor works in Posts
- Can create users with different roles
```

**Definition of Done:**
- [ ] All 6 collections visible in sidebar (Pages, Posts, Media, Categories, Users, Portfolio)
- [ ] Created test portfolio item with all 7 fields
- [ ] Created test category with all 6 fields
- [ ] Created test post with all 13 fields
- [ ] Created admin and editor users
- [ ] Verified access restrictions:
  - [ ] Admin can delete users, editor cannot
  - [ ] Admin can edit Header/Footer, editor cannot
  - [ ] Both can create/edit Posts/Pages
- [ ] Documented any issues

**測試腳本：**

```typescript
// 測試檔案: apps/backend/src/scripts/test-collections.ts
// 可選：創建自動化測試腳本

console.log('🧪 Testing Collections...')

// 1. 測試 Portfolio Collection
console.log('✓ Portfolio collection exists')
console.log('✓ Can create portfolio item')
console.log('✓ Image uploads to R2')

// 2. 測試 Categories Collection
console.log('✓ Categories collection has 6 fields')
console.log('✓ Color fields work')

// 3. 測試 Posts Collection
console.log('✓ Posts collection has 13 fields')
console.log('✓ Excerpt limits to 200 chars')
console.log('✓ OG image uploads')

// 4. 測試 Users Collection
console.log('✓ Users collection has role field')
console.log('✓ Can create admin user')
console.log('✓ Can create editor user')

// 5. 測試 Access Control
console.log('✓ Admin can delete users')
console.log('✓ Editor cannot delete users')
console.log('✓ Admin can edit Header/Footer')
console.log('✓ Editor cannot edit Header/Footer')
console.log('✓ Both can create/edit Posts')

console.log('✅ All tests passed!')
```

**預估時間：** 1 小時

---

## 🔗 相關 Stories 更新

### Story 1.9: Blog System（依賴 Story 1.2）

**更新後的依賴：**
```gherkin
Story 1.2 must be completed first:
- Categories collection needs textColor/backgroundColor for theming
- Posts collection needs excerpt for list display
- Posts collection needs status for filtering
```

**Task 1.9.2（Blog Listing Page）更新：**
```diff
+ 從 Payload API 載入已發布文章（status: 'published'）
+ 顯示 category badge（使用 category.textColor/backgroundColor）
+ 顯示文章摘要（使用 post.excerpt）
```

**Task 1.9.4（Category Page）更新：**
```diff
+ 分類顏色主題（使用 category.textColor/backgroundColor）
+ Badge 使用動態顏色
```

### Story 1.10: Portfolio（依賴 Story 1.2）

**更新後的依賴：**
```gherkin
Story 1.2 must be completed first:
- Portfolio collection must exist
- All 7 fields must be defined
```

**Task 1.10.1（Design Portfolio Architecture）更新：**
```diff
- 決定是否使用 Payload CMS collection
+ Portfolio collection 已在 Story 1.2 創建
- 欄位規劃
+ 欄位已在 Story 1.2 定義（title, slug, url, image, description, websiteType, tags）
```

**Task 1.10.2（Implement Portfolio Listing Page）更新：**
```diff
+ 2-column grid 佈局
+ Portfolio card 顯示：
+   - Project image (image)
+   - Project title (title)
+   - Description (description)
+   - Tags (tags array)
+   - Website type badge (websiteType)
```

### Story 1.12: Authentication（依賴 Story 1.2）

**更新後的依賴：**
```gherkin
Story 1.2 Phase 3 must be completed first:
- Users collection must have role field
- Access control functions must exist (adminOnly, adminOrEditor)
- All collections must apply access control
```

**Task 1.12.5（Implement Role-Based Access Control）更新：**
```diff
- Users collection 添加 role 欄位
+ Users collection 已在 Story 1.2 Task 1.2.4 添加 role 欄位
- 建立 access/adminOnly.ts
+ access/adminOnly.ts 已在 Story 1.2 Task 1.2.5 創建
- 建立 access/adminOrEditor.ts
+ access/adminOrEditor.ts 已在 Story 1.2 Task 1.2.5 創建
- 更新 Collections 的 access rules
+ Collections 已在 Story 1.2 Task 1.2.6 應用 access rules
+ 此 Task 改為測試驗證角色權限
```

---

## 📊 更新後的時間線

### Story 1.2: Payload CMS Collections Definition

| Phase | Task | 內容 | 預估時間 |
|-------|------|------|---------|
| Phase 1 | 1.2.1 | 創建 Portfolio Collection | 1 小時 |
| Phase 1 | 1.2.2 | 完善 Categories Collection | 30 分鐘 |
| Phase 2 | 1.2.3 | 完善 Posts Collection | 30 分鐘 |
| Phase 2 | 1.2.4 | 完善 Users Collection | 30 分鐘 |
| Phase 3 | 1.2.5 | 創建 Access Control 函數 | 30 分鐘 |
| Phase 3 | 1.2.6 | 應用 Access Control | 30 分鐘 |
| Phase 3 | 1.2.7 | 驗證和測試 | 1 小時 |
| **總計** | | | **4 小時** |

### Sprint 影響

**Sprint 0（原計劃）:** 8-12 小時
- Story 1.1: 40 分鐘
- Story 1.2: 4 小時 ← **更新後**
- **總計：** 4.5-5 小時（比原計劃減少約 50%）

**原因：**
- 原計劃高估了複雜度
- Payload CMS 的架構非常清晰
- Access control 模式簡單一致

---

## ✅ 最終驗收標準

### Collection 完整性
- [x] Portfolio: 7/7 欄位 ✅
- [x] Categories: 6/6 欄位 ✅
- [x] Posts: 13/13 欄位 ✅
- [x] Users: 4/4 欄位 ✅
- [x] Media: 100% ✅（已完整）
- [x] Pages: 100% ✅（已完整）

### Access Control 完整性
- [x] 5 個 access 函數 ✅
  - anyone ✅
  - authenticated ✅
  - authenticatedOrPublished ✅
  - adminOnly ✅（新增）
  - adminOrEditor ✅（新增）

### 功能驗收
- [x] 所有 collections 在 admin panel 可見
- [x] 所有欄位可編輯
- [x] 角色權限正確執行
- [x] Media 上傳到 R2
- [x] Rich text editor 正常
- [x] 型別生成成功

---

## 📝 備註

### Webflow 欄位對應表

#### Portfolio Collection
| Webflow 欄位 | Payload 欄位 | 轉換規則 |
|-------------|-------------|---------|
| Name | title | 直接對應 |
| Slug | slug | 保留原始值 |
| Website Link | url | 直接對應 |
| Preview Image | image | 上傳到 R2 |
| Description | description | 直接對應 |
| Website Type | websiteType | Select options |
| Tags | tags | String array |

#### Categories Collection
| Webflow 欄位 | Payload 欄位 | 轉換規則 |
|-------------|-------------|---------|
| Name | title | 直接對應 |
| Slug | slug | 保留原始值 |
| - | nameEn | ❌ 需手動新增 |
| - | order | ❌ 預設 0，手動調整 |
| Color | textColor/backgroundColor | 拆分為兩個欄位 |

#### Posts Collection
| Webflow 欄位 | Payload 欄位 | 轉換規則 |
|-------------|-------------|---------|
| Title | title | 直接對應 |
| Slug | slug | 保留原始值 |
| Body | content | Richtext → Lexical JSON |
| Published Date | publishedAt | ISO 8601 |
| Post Category | categories | 關聯 Categories |
| Featured Image | heroImage | 上傳到 R2 |
| SEO Title | meta.title | SEO plugin |
| SEO Description | meta.description | SEO plugin |
| - | excerpt | ❌ 需手動新增 |
| - | ogImage | ❌ 需手動上傳 |
| - | showInFooter | ❌ 預設 false |
| - | status | ❌ 根據 published 判斷 |

---

**文檔版本：** v1.0
**最後更新：** 2025-01-30
**適用於：** Payload CMS 3.59.1
**相關文檔：**
- `docs/prd/epic-1-execution-plan.md`
- `docs/prd/epic-1-stories-1.3-1.17-tasks.md`
- `docs/prd/payload-cms-slimming-report.md`
