Repository: ArvinYang1925/kaiso-meow-backend
Pull request #37: Feature/instructor courses
Author: login: peter6601
Labels: name: 優先權-中
Created: 2025-05-15T12:08:18Z | Merged: 2025-05-16T15:03:48Z

# Pull Request 概述

[32 創建課程 API](https://www.notion.so/POST-api-v1-instructor-courses-1d06a246851880dbb93ec6fa0f903175?pvs=4) 
[34 取得課程資料 API](https://www.notion.so/GET-api-v1-instructor-courses-id-1d06a24685188062a63cdb2f97aa9d1a?pvs=4) 
[35 更新課程 API](https://www.notion.so/PUT-api-v1-instructor-courses-id-1d06a2468518803f91d3fea0da7273c0?pvs=4)

---

### 本次 PR 的主要變更

請列出本次 PR 中引入的主要變更點，可以使用列表的形式：

* 新增3支API

---

### 詳細說明

用Zod去做每個欄位檢查
用middle檢查身份

---

### 測試計畫

使用postman去測試

* 創建 -> 查看
* 更新 -> 查看

**測試結果:**

成功查看
---

### Checklist

在提交 PR 之前，請確保你已經完成以下檢查：

* [x] 我已經閱讀並理解了 [貢獻指南](CONTRIBUTING.md 或其他相關文件)。
* [x] 我的程式碼風格與專案的風格保持一致。
* [x] 所有的測試都已通過。
* [x] 我已經更新了相關的文件 (如果適用)。
* [x] 我的提交資訊清晰明瞭。

---



Diff:
diff --git a/src/controllers/instructorCourseController.ts b/src/controllers/instructorCourseController.ts
new file mode 100644
index 0000000..1880033
--- /dev/null
+++ b/src/controllers/instructorCourseController.ts
@@ -0,0 +1,177 @@
+import { Response, NextFunction } from "express";
+import { createCourseSchema, updateCourseSchema } from "../validator/courseVaildationschema";
+import { AppDataSource } from "../config/db";
+import { Course } from "../entities/Course";
+import { AuthRequest } from "../middleware/isAuth";
+import { uuidSchema } from "../validator/commonValidationSchemas";
+
+/**
+ * API #32 POST - /api/v1/instructor/courses
+ *
+ * 📘 [API 文件 Notion 連結](https://www.notion.so/POST-api-v1-instructor-courses-1d06a246851880dbb93ec6fa0f903175?pvs=4)
+ *
+ * 此 API 講師可以創建課程
+ */
+export async function createCourse(req: AuthRequest, res: Response, next: NextFunction) {
+  try {
+    const result = createCourseSchema.safeParse(req.body);
+    if (!result.success) {
+      const err = result.error.errors[0];
+      res.status(400).json({ status: "failed", message: err.message });
+      return;
+    }
+
+    const userId = req.user?.id;
+
+    const { title, subtitle, description, highlight, duration, price, isFree, coverUrl } = result.data;
+    const courseRepo = AppDataSource.getRepository(Course);
+    const existingCourse = await courseRepo.findOne({ where: { title } });
+    if (existingCourse) {
+      res.status(400).json({
+        status: "failed",
+        message: "課程標題已存在，請使用其他名稱",
+      });
+      return;
+    }
+    const course = courseRepo.create({
+      title,
+      subtitle,
+      description,
+      highlight,
+      duration,
+      price: isFree ? 0 : price,
+      isFree,
+      coverUrl,
+      instructorId: userId, // ✅ 因為 courses 表的 FK 綁定的是 users.id
+      isPublished: false,
+    });
+
+    await courseRepo.save(course);
+
+    res.status(200).json({
+      status: "success",
+      message: "課程建立成功",
+      data: course,
+    });
+  } catch (err) {
+    next(err);
+  }
+}
+
+/**
+ * API #34 GET -/api/v1/instructor/courses/:id
+ *
+ * 📘 [API 文件 Notion 連結](https://www.notion.so/GET-api-v1-instructor-courses-id-1d06a24685188062a63cdb2f97aa9d1a?pvs=4)
+ *
+ * 此 API 讓講師可以查看單一課程詳細資訊
+ */
+export async function getCourseDetailByInstructor(req: AuthRequest, res: Response, next: NextFunction) {
+  try {
+    const userId = req.user?.id;
+    const courseId = req.params.id;
+    const parsed = uuidSchema.safeParse(courseId);
+
+    if (!parsed.success) {
+      res.status(400).json({ status: "failed", message: "無效的課程ID格式" });
+      return;
+    }
+
+    const courseRepo = AppDataSource.getRepository(Course);
+    const course = await courseRepo.findOne({
+      where: { id: courseId },
+    });
+
+    if (!course) {
+      res.status(404).json({
+        status: "failed",
+        message: "找不到指定課程",
+      });
+      return;
+    }
+
+    if (course.instructorId !== userId) {
+      res.status(403).json({
+        status: "failed",
+        message: "權限不足",
+      });
+      return;
+    }
+
+    res.status(200).json({
+      status: "success",
+      message: "課程資訊取得成功",
+      data: course,
+    });
+  } catch (err) {
+    next(err);
+  }
+}
+
+/**
+ * API #35 PUT - /api/v1/instructor/courses/:id
+ *
+ * 📘 [API 文件 Notion 連結](https://www.notion.so/PUT-api-v1-instructor-courses-id-1d06a2468518803f91d3fea0da7273c0?pvs=4)
+ *
+ * 此 API 讓講師可以編輯課程
+ */
+export async function updateCourseByInstructor(req: AuthRequest, res: Response, next: NextFunction) {
+  try {
+    const result = updateCourseSchema.safeParse(req.body);
+    if (!result.success) {
+      const err = result.error.errors[0];
+      res.status(400).json({ status: "failed", message: err.message });
+      return;
+    }
+
+    const userId = req.user?.id;
+    const courseId = req.params.id;
+    const parsed = uuidSchema.safeParse(courseId);
+
+    if (!parsed.success) {
+      res.status(400).json({ status: "failed", message: "無效的課程ID格式" });
+      return;
+    }
+    const courseRepo = AppDataSource.getRepository(Course);
+    const course = await courseRepo.findOne({ where: { id: courseId } });
+
+    if (!course) {
+      res.status(404).json({ status: "failed", message: "找不到指定課程" });
+      return;
+    }
+
+    if (course.instructorId !== userId) {
+      res.status(403).json({ status: "failed", message: "權限不足" });
+      return;
+    }
+
+    const { title, subtitle, description, highlight, duration, price, isFree, coverUrl } = result.data;
+    const existingCourse = await courseRepo.findOne({ where: { title } });
+
+    if (existingCourse && existingCourse.id !== course.id) {
+      res.status(400).json({
+        status: "failed",
+        message: "課程標題已存在，請使用其他名稱",
+      });
+      return;
+    }
+
+    course.title = title;
+    course.subtitle = subtitle;
+    course.description = description;
+    course.highlight = highlight;
+    course.duration = duration;
+    course.isFree = isFree;
+    course.price = isFree ? 0 : price;
+    course.coverUrl = coverUrl;
+
+    await courseRepo.save(course);
+
+    res.status(200).json({
+      status: "success",
+      message: "課程更新成功",
+      data: course,
+    });
+  } catch (err) {
+    next(err);
+  }
+}
diff --git a/src/routes/instructorRoutes.ts b/src/routes/instructorRoutes.ts
index 6d30723..d5a8483 100644
--- a/src/routes/instructorRoutes.ts
+++ b/src/routes/instructorRoutes.ts
@@ -2,10 +2,14 @@ import { Router } from "express";
 import { getMe, updateMe } from "../controllers/instructorController";
 import { isInstructor } from "../middleware/isInstructor";
 import { isAuth } from "../middleware/isAuth";
+import { createCourse, getCourseDetailByInstructor, updateCourseByInstructor } from "../controllers/instructorCourseController";
 
 const router = Router();
 
 router.get("/me", isAuth, isInstructor, getMe);
 router.put("/me", isAuth, isInstructor, updateMe);
+router.post("/courses", isAuth, isInstructor, createCourse);
+router.put("/courses/:id", isAuth, isInstructor, updateCourseByInstructor);
+router.get("/courses/:id", isAuth, isInstructor, getCourseDetailByInstructor);
 
 export default router;
diff --git a/src/validator/courseVaildationschema.ts b/src/validator/courseVaildationschema.ts
new file mode 100644
index 0000000..14e3878
--- /dev/null
+++ b/src/validator/courseVaildationschema.ts
@@ -0,0 +1,32 @@
+import { z } from "zod";
+
+export const titleSchema = z.string().min(1, { message: "課程名為必填" }).max(30, { message: "課程名長度需少於 30 個字元" });
+export const subtitleSchema = z.string().max(50, { message: "課程名長度需少於 50 個字元" }).optional();
+export const descriptionSchema = z.string().min(1, { message: "敘述為必填" });
+export const durationSchema = z.number().positive({ message: "長度數值要大於0" });
+export const highlightSchema = z.string({ message: "highlight必須要是字串" }).optional();
+export const priceSchema = z.number().int().nonnegative({ message: "數值不能為負數" });
+export const isFreeSchema = z.boolean({ message: "isFree必須為布林" });
+export const coverUrlSchema = z.string().url({ message: "coverUrl必須是合法的url" }).max(2048).optional();
+
+export const createCourseSchema = z.object({
+  title: titleSchema,
+  subtitle: subtitleSchema,
+  description: descriptionSchema,
+  highlight: highlightSchema,
+  duration: durationSchema,
+  price: priceSchema,
+  isFree: isFreeSchema,
+  coverUrl: coverUrlSchema,
+});
+
+export const updateCourseSchema = z.object({
+  title: titleSchema,
+  subtitle: subtitleSchema,
+  description: descriptionSchema,
+  highlight: highlightSchema,
+  duration: durationSchema,
+  price: priceSchema,
+  isFree: isFreeSchema,
+  coverUrl: coverUrlSchema,
+});


Reviews:
user: login: CHING-WENLAI1031
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T08:43:14Z
user: login: CHING-WENLAI1031
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T08:49:35Z
user: login: CHING-WENLAI1031
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T09:13:23Z
user: login: CHING-WENLAI1031
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T09:14:30Z
user: login: peter6601
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T10:08:16Z
user: login: peter6601
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T10:08:24Z
user: login: peter6601
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T10:08:32Z
user: login: peter6601
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T10:12:10Z
user: login: CHING-WENLAI1031
body: Nice
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-16T15:02:37Z

Review comments:
diff_hunk: @@ -0,0 +1,149 @@
+import { Response, NextFunction } from "express";
+import { createCourseSchema, updateCourseSchema } from "../validator/courseVaildationschema";
+import { AppDataSource } from "../config/db";
+import { Course } from "../entities/Course";
+import { AuthRequest } from "../middleware/isAuth";
+
+/**
+ * API #32 POST - /api/v1/instructor/courses
+ *
+ * 📘 [API 文件 Notion 連結](https://www.notion.so/POST-api-v1-instructor-courses-1d06a246851880dbb93ec6fa0f903175?pvs=4)
+ *
+ * 此 API 講師可以創建課程
+ */
+export async function createCourse(req: AuthRequest, res: Response, next: NextFunction) {
+  try {
+    const result = createCourseSchema.safeParse(req.body);
+    if (!result.success) {
+      const err = result.error.errors[0];
+      res.status(400).json({ status: "failed", message: err.message });
+      return;
+    }
+
+    const userId = req.user?.id;
+
+    const { title, subtitle, description, highlight, duration, price, isFree, coverUrl } = result.data;
+
+    const courseRepo = AppDataSource.getRepository(Course);
+    const course = courseRepo.create({
+      title,
+      subtitle,
+      description,
+      highlight,
+      duration,
+      price: isFree ? 0 : price,
+      isFree,
+      coverUrl,
+      instructorId: userId, // ✅ 因為 courses 表的 FK 綁定的是 users.id
+      isPublished: false,
+    });
+
+    await courseRepo.save(course);
+
+    res.status(200).json({
+      status: "success",
+      message: "課程建立成功",
+      data: course,
+    });
+  } catch (err) {
+    next(err);
+  }
+}
+
+/**
+ * API #34 GET -/api/v1/instructor/courses/:id
+ *
+ * 📘 [API 文件 Notion 連結](https://www.notion.so/GET-api-v1-instructor-courses-id-1d06a24685188062a63cdb2f97aa9d1a?pvs=4)
+ *
+ * 此 API 讓講師可以查看單一課程詳細資訊
+ */
+export async function getCourseDetailByInstructor(req: AuthRequest, res: Response, next: NextFunction) {
+  try {
+    const userId = req.user?.id;
+    const courseId = req.params.id;
+
path: src/controllers/instructorCourseController.ts
user: login: CHING-WENLAI1031
body: 可以加uuid不合規的阻擋～commonValidation裡面有可以用
![image](https://github.com/user-attachments/assets/fcf4fe6e-39d2-4fff-809d-715569b37d5b)

created_at: 2025-05-16T08:43:14Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,149 @@
+import { Response, NextFunction } from "express";
+import { createCourseSchema, updateCourseSchema } from "../validator/courseVaildationschema";
+import { AppDataSource } from "../config/db";
+import { Course } from "../entities/Course";
+import { AuthRequest } from "../middleware/isAuth";
+
+/**
+ * API #32 POST - /api/v1/instructor/courses
+ *
+ * 📘 [API 文件 Notion 連結](https://www.notion.so/POST-api-v1-instructor-courses-1d06a246851880dbb93ec6fa0f903175?pvs=4)
+ *
+ * 此 API 講師可以創建課程
+ */
+export async function createCourse(req: AuthRequest, res: Response, next: NextFunction) {
+  try {
+    const result = createCourseSchema.safeParse(req.body);
+    if (!result.success) {
+      const err = result.error.errors[0];
+      res.status(400).json({ status: "failed", message: err.message });
+      return;
+    }
+
path: src/controllers/instructorCourseController.ts
user: login: CHING-WENLAI1031
body: <img width="829" alt="image" src="https://github.com/user-attachments/assets/f70d8165-5d5c-4c27-9178-0e4cb015affc" />

是否該檢查課程title不能重複＠＠
created_at: 2025-05-16T08:49:35Z
author_association: COLLABORATOR
path: src/validator/courseVaildationschema.ts
user: login: CHING-WENLAI1031
body: 文件可以更新一下，列出這些檢核
https://www.notion.so/PUT-api-v1-instructor-courses-id-1d06a2468518803f91d3fea0da7273c0
https://www.notion.so/POST-api-v1-instructor-courses-1d06a246851880dbb93ec6fa0f903175
created_at: 2025-05-16T09:13:22Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,149 @@
+import { Response, NextFunction } from "express";
+import { createCourseSchema, updateCourseSchema } from "../validator/courseVaildationschema";
+import { AppDataSource } from "../config/db";
+import { Course } from "../entities/Course";
+import { AuthRequest } from "../middleware/isAuth";
+
+/**
+ * API #32 POST - /api/v1/instructor/courses
+ *
+ * 📘 [API 文件 Notion 連結](https://www.notion.so/POST-api-v1-instructor-courses-1d06a246851880dbb93ec6fa0f903175?pvs=4)
+ *
+ * 此 API 講師可以創建課程
+ */
+export async function createCourse(req: AuthRequest, res: Response, next: NextFunction) {
+  try {
+    const result = createCourseSchema.safeParse(req.body);
+    if (!result.success) {
+      const err = result.error.errors[0];
+      res.status(400).json({ status: "failed", message: err.message });
+      return;
+    }
+
+    const userId = req.user?.id;
+
+    const { title, subtitle, description, highlight, duration, price, isFree, coverUrl } = result.data;
+
+    const courseRepo = AppDataSource.getRepository(Course);
+    const course = courseRepo.create({
+      title,
+      subtitle,
+      description,
+      highlight,
+      duration,
+      price: isFree ? 0 : price,
+      isFree,
+      coverUrl,
+      instructorId: userId, // ✅ 因為 courses 表的 FK 綁定的是 users.id
+      isPublished: false,
+    });
+
+    await courseRepo.save(course);
+
+    res.status(200).json({
+      status: "success",
+      message: "課程建立成功",
+      data: course,
+    });
+  } catch (err) {
+    next(err);
+  }
+}
+
+/**
+ * API #34 GET -/api/v1/instructor/courses/:id
+ *
+ * 📘 [API 文件 Notion 連結](https://www.notion.so/GET-api-v1-instructor-courses-id-1d06a24685188062a63cdb2f97aa9d1a?pvs=4)
+ *
+ * 此 API 讓講師可以查看單一課程詳細資訊
+ */
+export async function getCourseDetailByInstructor(req: AuthRequest, res: Response, next: NextFunction) {
+  try {
+    const userId = req.user?.id;
+    const courseId = req.params.id;
+
+    const courseRepo = AppDataSource.getRepository(Course);
+    const course = await courseRepo.findOne({
+      where: { id: courseId },
+    });
+
+    if (!course) {
+      res.status(404).json({
+        status: "failed",
+        message: "找不到指定課程",
+      });
+      return;
+    }
+
+    if (course.instructorId !== userId) {
+      res.status(403).json({
+        status: "failed",
+        message: "權限不足",
+      });
+      return;
+    }
+
+    res.status(200).json({
+      status: "success",
+      message: "課程資訊取得成功",
+      data: course,
+    });
+  } catch (err) {
+    next(err);
+  }
+}
+
+/**
+ * API #35 PUT - /api/v1/instructor/courses/:id
+ *
+ * 📘 [API 文件 Notion 連結](https://www.notion.so/PUT-api-v1-instructor-courses-id-1d06a2468518803f91d3fea0da7273c0?pvs=4)
+ *
+ * 此 API 讓講師可以編輯課程
+ */
+export async function updateCourseByInstructor(req: AuthRequest, res: Response, next: NextFunction) {
path: src/controllers/instructorCourseController.ts
user: login: CHING-WENLAI1031
body: 跟POST一樣，看要不要檢查titlet重複跟uuid的阻擋
created_at: 2025-05-16T09:14:30Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,149 @@
+import { Response, NextFunction } from "express";
+import { createCourseSchema, updateCourseSchema } from "../validator/courseVaildationschema";
+import { AppDataSource } from "../config/db";
+import { Course } from "../entities/Course";
+import { AuthRequest } from "../middleware/isAuth";
+
+/**
+ * API #32 POST - /api/v1/instructor/courses
+ *
+ * 📘 [API 文件 Notion 連結](https://www.notion.so/POST-api-v1-instructor-courses-1d06a246851880dbb93ec6fa0f903175?pvs=4)
+ *
+ * 此 API 講師可以創建課程
+ */
+export async function createCourse(req: AuthRequest, res: Response, next: NextFunction) {
+  try {
+    const result = createCourseSchema.safeParse(req.body);
+    if (!result.success) {
+      const err = result.error.errors[0];
+      res.status(400).json({ status: "failed", message: err.message });
+      return;
+    }
+
path: src/controllers/instructorCourseController.ts
user: login: peter6601
body: 已加入，感謝提醒
created_at: 2025-05-16T10:08:15Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,149 @@
+import { Response, NextFunction } from "express";
+import { createCourseSchema, updateCourseSchema } from "../validator/courseVaildationschema";
+import { AppDataSource } from "../config/db";
+import { Course } from "../entities/Course";
+import { AuthRequest } from "../middleware/isAuth";
+
+/**
+ * API #32 POST - /api/v1/instructor/courses
+ *
+ * 📘 [API 文件 Notion 連結](https://www.notion.so/POST-api-v1-instructor-courses-1d06a246851880dbb93ec6fa0f903175?pvs=4)
+ *
+ * 此 API 講師可以創建課程
+ */
+export async function createCourse(req: AuthRequest, res: Response, next: NextFunction) {
+  try {
+    const result = createCourseSchema.safeParse(req.body);
+    if (!result.success) {
+      const err = result.error.errors[0];
+      res.status(400).json({ status: "failed", message: err.message });
+      return;
+    }
+
+    const userId = req.user?.id;
+
+    const { title, subtitle, description, highlight, duration, price, isFree, coverUrl } = result.data;
+
+    const courseRepo = AppDataSource.getRepository(Course);
+    const course = courseRepo.create({
+      title,
+      subtitle,
+      description,
+      highlight,
+      duration,
+      price: isFree ? 0 : price,
+      isFree,
+      coverUrl,
+      instructorId: userId, // ✅ 因為 courses 表的 FK 綁定的是 users.id
+      isPublished: false,
+    });
+
+    await courseRepo.save(course);
+
+    res.status(200).json({
+      status: "success",
+      message: "課程建立成功",
+      data: course,
+    });
+  } catch (err) {
+    next(err);
+  }
+}
+
+/**
+ * API #34 GET -/api/v1/instructor/courses/:id
+ *
+ * 📘 [API 文件 Notion 連結](https://www.notion.so/GET-api-v1-instructor-courses-id-1d06a24685188062a63cdb2f97aa9d1a?pvs=4)
+ *
+ * 此 API 讓講師可以查看單一課程詳細資訊
+ */
+export async function getCourseDetailByInstructor(req: AuthRequest, res: Response, next: NextFunction) {
+  try {
+    const userId = req.user?.id;
+    const courseId = req.params.id;
+
path: src/controllers/instructorCourseController.ts
user: login: peter6601
body: 已加入，感謝提醒
created_at: 2025-05-16T10:08:24Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,149 @@
+import { Response, NextFunction } from "express";
+import { createCourseSchema, updateCourseSchema } from "../validator/courseVaildationschema";
+import { AppDataSource } from "../config/db";
+import { Course } from "../entities/Course";
+import { AuthRequest } from "../middleware/isAuth";
+
+/**
+ * API #32 POST - /api/v1/instructor/courses
+ *
+ * 📘 [API 文件 Notion 連結](https://www.notion.so/POST-api-v1-instructor-courses-1d06a246851880dbb93ec6fa0f903175?pvs=4)
+ *
+ * 此 API 講師可以創建課程
+ */
+export async function createCourse(req: AuthRequest, res: Response, next: NextFunction) {
+  try {
+    const result = createCourseSchema.safeParse(req.body);
+    if (!result.success) {
+      const err = result.error.errors[0];
+      res.status(400).json({ status: "failed", message: err.message });
+      return;
+    }
+
+    const userId = req.user?.id;
+
+    const { title, subtitle, description, highlight, duration, price, isFree, coverUrl } = result.data;
+
+    const courseRepo = AppDataSource.getRepository(Course);
+    const course = courseRepo.create({
+      title,
+      subtitle,
+      description,
+      highlight,
+      duration,
+      price: isFree ? 0 : price,
+      isFree,
+      coverUrl,
+      instructorId: userId, // ✅ 因為 courses 表的 FK 綁定的是 users.id
+      isPublished: false,
+    });
+
+    await courseRepo.save(course);
+
+    res.status(200).json({
+      status: "success",
+      message: "課程建立成功",
+      data: course,
+    });
+  } catch (err) {
+    next(err);
+  }
+}
+
+/**
+ * API #34 GET -/api/v1/instructor/courses/:id
+ *
+ * 📘 [API 文件 Notion 連結](https://www.notion.so/GET-api-v1-instructor-courses-id-1d06a24685188062a63cdb2f97aa9d1a?pvs=4)
+ *
+ * 此 API 讓講師可以查看單一課程詳細資訊
+ */
+export async function getCourseDetailByInstructor(req: AuthRequest, res: Response, next: NextFunction) {
+  try {
+    const userId = req.user?.id;
+    const courseId = req.params.id;
+
+    const courseRepo = AppDataSource.getRepository(Course);
+    const course = await courseRepo.findOne({
+      where: { id: courseId },
+    });
+
+    if (!course) {
+      res.status(404).json({
+        status: "failed",
+        message: "找不到指定課程",
+      });
+      return;
+    }
+
+    if (course.instructorId !== userId) {
+      res.status(403).json({
+        status: "failed",
+        message: "權限不足",
+      });
+      return;
+    }
+
+    res.status(200).json({
+      status: "success",
+      message: "課程資訊取得成功",
+      data: course,
+    });
+  } catch (err) {
+    next(err);
+  }
+}
+
+/**
+ * API #35 PUT - /api/v1/instructor/courses/:id
+ *
+ * 📘 [API 文件 Notion 連結](https://www.notion.so/PUT-api-v1-instructor-courses-id-1d06a2468518803f91d3fea0da7273c0?pvs=4)
+ *
+ * 此 API 讓講師可以編輯課程
+ */
+export async function updateCourseByInstructor(req: AuthRequest, res: Response, next: NextFunction) {
path: src/controllers/instructorCourseController.ts
user: login: peter6601
body: 已加入，感謝提醒
created_at: 2025-05-16T10:08:32Z
author_association: COLLABORATOR
path: src/validator/courseVaildationschema.ts
user: login: peter6601
body: 已加入，感謝提醒

- 課程ID格式錯誤 → 回傳 400
- 標題跟其他課程標題重複→ 回傳 400

created_at: 2025-05-16T10:12:10Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-15T12:49:50Z
body: ### ✨ 變更說明（Change Summary）

本次 PR 旨在實作講師課程管理相關的 API，包含新增課程、取得課程詳細資訊及更新課程的功能。這些 API 讓講師可以在平台上管理自己的課程內容。

---

### ✅ 本次提交包含

- [x] 🆕 新功能開發  
- [ ] 🐞 錯誤修正  
- [ ] 📝 文件調整  
- [ ] 🎨 UI/UX 優化  
- [ ] 🧪 單元測試 / CI 配置  
- [ ] 🔧 其他技術調整（請補充）

---

### 🔍 變更內容摘要（What Was Changed）

- 新增 `src/controllers/instructorCourseController.ts`，包含以下 API 實作：
    - `createCourse`: 講師新增課程 API
    - `getCourseDetailByInstructor`: 講師查詢課程詳細資訊 API
    - `updateCourseByInstructor`: 講師更新課程資訊 API
- 修改 `src/routes/instructorRoutes.ts`，新增課程管理相關 API 的路由：
    - `POST /courses`:  新增課程
    - `GET /courses/:id`: 查詢課程詳細資訊
    - `PUT /courses/:id`: 更新課程資訊
- 新增 `src/validator/courseVaildationschema.ts`，新增課程驗證 schema：
    - `createCourseSchema`: 新增課程資料驗證
    - `updateCourseSchema`: 更新課程資料驗證

---

### 🧪 測試與驗證

- [x] 本地測試通過
- [x] CI 檢查通過
- [x] 已驗證對應 API 功能正常運作
- [ ] 無破壞既有功能

---

### 🚨 注意事項

-  需要確認資料庫 `courses` 表的 `instructorId` 外鍵正確關聯到 `users` 表的 `id` 欄位。
-  在部署前，請確保 `COURSE_IMAGE_UPLOAD_PATH` 環境變數已設定正確，用於儲存課程封面圖片。 (雖然程式碼中沒有直接顯示此部分，但是課程管理通常會涉及到圖片上傳，所以備註)
-  如果未來需要增加更多課程欄位，請記得更新 `createCourseSchema` 和 `updateCourseSchema`。
-  目前沒有單元測試，建議後續補上。

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-16T10:07:50Z
body: ### ✨ 變更說明（Change Summary）

本次 PR 旨在新增講師課程管理功能，讓講師可以透過後台建立、查看、更新課程。主要新增了講師建立課程、查看單一課程資訊、更新課程的 API 及其相關的驗證機制。

---

### ✅ 本次提交包含

- [x] 🆕 新功能開發
- [ ] 🐞 錯誤修正
- [ ] 📝 文件調整 （Notion API 文件已更新，但程式碼註解中的連結待更新至正式環境）
- [ ] 🎨 UI/UX 優化 （後端 API，無 UI 變動）
- [ ] 🧪 單元測試 / CI 配置 （待新增）
- [ ] 🔧 其他技術調整（請補充）

---

### 🔍 變更內容摘要（What Was Changed）

- 新增 `src/controllers/instructorCourseController.ts` 檔案，包含 `createCourse`、`getCourseDetailByInstructor`、`updateCourseByInstructor` 三個 controller 函式，分別用於處理講師建立課程、查看課程詳情、更新課程的功能。
- 新增 `src/validator/courseVaildationschema.ts` 檔案，定義了課程相關資料的驗證 schema，包括 `createCourseSchema` 和 `updateCourseSchema`，使用 Zod 進行驗證。
- 修改 `src/routes/instructorRoutes.ts` 檔案，新增了三個 endpoint，分別對應到 `createCourse`、`getCourseDetailByInstructor`、`updateCourseByInstructor` 三個 controller，並加入了 `isAuth` 和 `isInstructor` 中介層進行權限驗證。

---

### 🧪 測試與驗證

- [x] 本地測試通過
- [ ] CI 檢查通過 （待設定）
- [x] 已驗證對應 API 功能正常運作
- [x] 無破壞既有功能 （需完整測試驗證）

---

### 🚨 注意事項

- 需要更新 Notion API 文件連結至正式環境連結。
- 需要新增單元測試，覆蓋 controllers 和 validation schemas。
- CI/CD 配置需要新增，以確保程式碼品質。
- 此 PR 導入了 Zod 作為驗證工具，請確保團隊成員了解其使用方式。
-  `src/controllers/instructorCourseController.ts` 中的註解使用了 Notion 連結，開發階段可使用，上線前須確認並修正。


author_association: NONE