Deploy Checkpoint
Day 4 - ชั่วโมงที่ 4: Update Status และ Deploy
เป้าหมายของชั่วโมงนี้
หลังจบชั่วโมงนี้ ผู้เรียนจะสามารถ:
- เปลี่ยน Status ของ Issue ใน Supabase ได้
- ใช้ Server Action สำหรับ Update ข้อมูลได้
- อธิบายความต่างระหว่าง Close, Soft Delete และ Hard Delete ได้
- ตรวจ Project ก่อนนำขึ้น Production ได้
- Deploy Next.js พร้อม Environment Variables ไปยัง Vercel ได้
ไฟล์ที่ใช้ในชั่วโมงนี้
lib/issues.ts เพิ่ม Function สำหรับ Update
app/actions.ts เพิ่ม Server Action สำหรับ Update
components/IssueList.tsx เปลี่ยนปุ่ม Client เป็น Form Actionขั้นตอน Deploy ใช้ GitHub, Vercel Dashboard และ Supabase Dashboard
Slide 1: จาก Create ไปสู่ Update
Hour 3 ทำให้ระบบสร้าง Issue ใหม่และบันทึกลง Supabase ได้แล้ว:
IssueForm → Server Action → Supabase INSERTHour นี้เราจะทำให้ปุ่ม Status บันทึกค่าลง Database เช่นกัน:
ปุ่ม Status → Server Action → Supabase UPDATEเมื่อ Create และ Update ทำงานครบ เราจะตรวจ Production Build แล้ว Deploy Project ไปยัง Vercel
สิ่งที่ระบบจะทำได้เมื่อจบ Day 4
- อ่านรายการและรายละเอียดจาก Supabase
- สร้าง Issue ใหม่
- เปลี่ยน Status เป็น
OPEN,IN_PROGRESSหรือDONE - เก็บข้อมูลไว้หลัง Refresh
- เปิดใช้งานผ่าน Production URL
Slide 2: สร้าง updateIssueStatus()
เพิ่ม Function นี้ต่อจาก createIssue() ใน lib/issues.ts:
export async function updateIssueStatus(
id: string,
status: IssueStatus,
): Promise<void> {
const supabase = createSupabaseServerClient();
const { error } = await supabase
.from("issues")
.update({
status,
updated_at: new Date().toISOString(),
})
.eq("id", id);
if (error) {
throw new Error(`Failed to update issue: ${error.message}`);
}
}IssueStatus ถูก Import อยู่แล้วจาก Hour 2–3 ให้ตรวจว่า Type Import ด้านบนมีครบ:
import type { Issue, IssueStatus, NewIssueInput } from "@/types/issue";อ่าน Query ทีละส่วน
.from("issues")เลือก Table.update({...})กำหนดค่าใหม่ของstatusและupdated_at.eq("id", id)จำกัดให้ Update เฉพาะ Row ที่มี ID ตรงกันif (error)หยุดการทำงานเมื่อ Supabase ปฏิเสธคำสั่ง
ค่า Default now() ของ updated_at ทำงานตอน Insert เท่านั้น Project นี้ยังไม่มี Database Trigger จึงกำหนดเวลาใหม่ทุกครั้งที่ Update
ห้ามลืม .eq("id", id) เพราะคำสั่ง Update ที่ไม่มีเงื่อนไขอาจกระทบหลาย Row
Slide 3: สร้าง Server Action สำหรับ Update
แก้ Import เดิมด้านบน app/actions.ts ให้รวม Function และ Type ที่ต้องใช้:
import { createIssue, updateIssueStatus } from "@/lib/issues";
import type { IssueStatus, NewIssueInput } from "@/types/issue";จากนั้นเพิ่ม Code นี้ต่อจาก createIssueAction():
const allowedStatuses: IssueStatus[] = ["OPEN", "IN_PROGRESS", "DONE"];
export async function updateIssueStatusAction(
formData: FormData,
): Promise<void> {
const id = String(formData.get("id") ?? "").trim();
const status = String(formData.get("status") ?? "").trim();
if (!id || !allowedStatuses.includes(status as IssueStatus)) {
throw new Error("Invalid update request");
}
await updateIssueStatus(id, status as IssueStatus);
revalidatePath("/issues");
}ทำไมต้องมี allowedStatuses
TypeScript ช่วยตรวจ Code ตอนพัฒนา แต่ค่าจาก FormData มาจาก Request และยังเป็น String ที่ผู้ใช้แก้ไขได้ Server Action จึงต้องตรวจว่าค่าอยู่ในรายการที่อนุญาตก่อนส่งเข้า Database
status อยู่ใน allowedStatuses → Update
status เป็นค่าอื่น → หยุดและแจ้ง ErrorAction นี้ไม่ใช้ redirect() เพราะผู้ใช้ยังอยู่หน้า /issues เดิม เราต้องการเพียง Update แล้วให้หน้ารายการอ่านข้อมูลล่าสุด
Slide 4: เปลี่ยน IssueList ให้เรียก Server Action
แก้บางส่วนใน components/IssueList.tsx
1. เปลี่ยน Import และลบ Client Boundary
เพิ่ม Import:
import { updateIssueStatusAction } from "@/app/actions";ลบ "use client" เพราะหลังเปลี่ยนเสร็จไฟล์นี้จะไม่มี onClick, State หรือ Browser API แล้ว
เก็บ Array นี้ไว้ตามเดิม:
const statuses: IssueStatus[] = ["OPEN", "IN_PROGRESS", "DONE"];2. ลด Props ให้เหลือเฉพาะข้อมูล
แทน Props Type และ Parameter เดิมด้วย:
type IssueListProps = {
issues: Issue[];
};
export function IssueList({ issues }: IssueListProps) {ลบ onUpdateStatus ออกจาก Props เพราะปุ่มจะเรียก Server Action โดยตรง
3. แสดง Column จัดการ
แทน <th> เดิมที่มีเงื่อนไข onUpdateStatus && (...) ด้วย:
<th className="px-3 py-3 font-semibold">จัดการ</th>4. แทนปุ่มเดิมในแต่ละ Row
แทน <td> เดิมที่มี onClick ด้วย Code นี้:
<td className="px-3 py-4">
<div className="flex flex-wrap gap-2">
{statuses.map((status) => (
<form key={status} action={updateIssueStatusAction}>
<input type="hidden" name="id" value={issue.id} />
<input type="hidden" name="status" value={status} />
<button
type="submit"
disabled={issue.status === status}
className="rounded-md border border-slate-300 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-slate-50 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:text-slate-400"
>
{status}
</button>
</form>
))}
</div>
</td>Hidden Input ส่ง id และ status ไปใน FormData ส่วนปุ่มของ Status ปัจจุบันถูก Disable เพื่อไม่ให้ Update ค่าเดิมซ้ำ
Slide 5: ทดสอบ Update Flow
- เปิดหน้า
/issues - เลือก Issue ที่ยังไม่เป็น
DONE - กด Status ใหม่ เช่น
IN_PROGRESS - ตรวจว่า Badge บนหน้า List เปลี่ยน
- Refresh หน้าและตรวจว่าค่าไม่ย้อนกลับ
- เปิด Supabase Table Editor แล้วตรวจ
statusและupdated_at - เปลี่ยน Status เป็น
DONEเพื่อทดสอบการปิดงาน
Flow ที่กำลังทดสอบ
กดปุ่ม
→ hidden inputs สร้าง FormData
→ updateIssueStatusAction()
→ updateIssueStatus()
→ Supabase UPDATE
→ revalidatePath("/issues")
→ หน้า List แสดงข้อมูลล่าสุดถ้า Update ไม่สำเร็จ
permission denied: ตรวจGRANT UPDATEและ RLS Policy จาก Hour 1Invalid update request: ตรวจชื่อ Hidden Inputidและstatus- กดแล้วไม่เกิดอะไรขึ้น: ตรวจ
action={updateIssueStatusAction} - Status ไม่เปลี่ยนใน Database: ตรวจ
.eq("id", id)และ Error ใน Terminal - Database เปลี่ยนแต่หน้าเก่า: ตรวจ
revalidatePath("/issues")
Slide 6: ตอนนี้ใคร Update Status ได้บ้าง
สไลด์นี้เป็นการทบทวน ยังไม่ต้องแก้โค้ดหรือรัน SQL เพิ่ม
การใช้ Server Action ช่วยย้าย Logic มาที่ Server แต่ยังไม่ได้สร้าง Authentication หรือ Authorization
ใน Hour 1 เราเปิด Demo Permission ให้ทั้ง anon และ authenticated:
grant select, insert, update
on public.issues
to anon, authenticated;
create policy "demo_update_issues"
on public.issues
for update
to anon, authenticated
using (true)
with check (true);ผลคือ Request ที่ยังไม่ Login ก็ Update Issue ทุก Row ได้ Policy นี้ใช้เพื่อฝึก CRUD เท่านั้น ไม่เหมาะกับข้อมูลจริง
ระบบจริงต้องตรวจหลายชั้น
- Authentication ตรวจว่า Request มาจากใคร
- Server Action ตรวจว่า User มี Role ที่อนุญาตหรือไม่
- RLS ตรวจซ้ำว่า User แก้ Row นี้ได้จริงหรือไม่
การซ่อนปุ่มบนหน้าเว็บไม่ใช่การตรวจสิทธิ์ เพราะผู้ใช้ยังสร้าง Request เองได้ Day 5 จะเพิ่ม Login, Role USER/ADMIN และเปลี่ยน Demo Policy ให้เป็น Policy ที่ตรวจผู้ใช้จริง
ดังนั้นตอนนี้ให้ใช้ Demo Policy เดิมต่อไปก่อน แล้วค่อยปรับสิทธิ์ใน Day 5 หลังจากระบบรู้แล้วว่าใคร Login อยู่และแต่ละคนมี Role อะไร
Slide 7: ทำไมใช้ DONE แทนการลบ
ระบบแจ้งปัญหาควรเก็บประวัติไว้ตรวจสอบย้อนหลัง เราจึงปิดงานด้วย Status DONE แทนการลบ Row
OPEN → IN_PROGRESS → DONE| วิธี | สิ่งที่เกิดกับข้อมูล | ใช้เมื่อ |
|---|---|---|
| Close | เปลี่ยน Status แต่ยังเห็นประวัติ | งานเสร็จแล้ว |
| Soft Delete | กำหนดค่า เช่น deleted_at แล้วซ่อนจาก Query ปกติ | ต้องการนำออกจาก UI แต่ยังเก็บข้อมูล |
| Hard Delete | ลบ Row ออกจาก Database | ข้อมูลต้องถูกลบจริงและผ่านการยืนยันสิทธิ์แล้ว |
Project นี้ใช้เฉพาะ Close เพราะเพียงพอกับระบบแจ้งปัญหาและไม่ทำให้ Scope ใหญ่เกินไป
คำสั่ง DELETE ไม่ได้ถูก Grant ให้ anon หรือ authenticated ใน Demo Permission ของเรา
Slide 8: ตรวจ Project ก่อน Deploy
1. ตรวจ Production Build
npm run buildProduction Build ช่วยตรวจ TypeScript, Import, Route และ Code ที่อาจทำงานได้ใน Development แต่ไม่ผ่านตอน Deploy
2. ตรวจ Git และ Environment Variables
git statusตรวจว่า .env.local ไม่ถูกเตรียม Commit และ .env.example มีเฉพาะชื่อ Variable:
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=3. Commit และ Push
หลังตรวจรายการไฟล์แล้ว:
git add .
git commit -m "Connect issue update flow to Supabase"
git pushVercel จะ Deploy จาก Code ที่อยู่ใน Git Repository จึงต้อง Push Commit ล่าสุดก่อน Import Project หรือรอ Deployment ใหม่
Slide 9: Import Project และ Deploy บน Vercel
- เปิด Vercel Dashboard แล้วเลือกสร้าง Project ใหม่
- เลือก Git Provider และ Import Repository ของ Project
- ตรวจ Framework Preset เป็น
Next.js - ตรวจ Root Directory ให้ตรงกับ Folder ที่มี
package.json - เพิ่ม Environment Variables สองค่า:
NEXT_PUBLIC_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEYคัดลอกค่าจาก .env.local โดยไม่ใส่เครื่องหมายคำพูดหรือช่องว่างเกินมา และเลือกให้ใช้กับ Environment Production
- กด Deploy และรอให้ Build สำเร็จ
- เปิด Production URL ที่ Vercel สร้างให้
ไฟล์ .env.local อยู่เฉพาะในเครื่องและไม่ได้ถูกส่งขึ้น Vercel จึงต้องตั้งค่า Variables ใน Project Settings แยกต่างหาก
ถ้าแก้ Environment Variables ภายหลัง ค่าใหม่จะมีผลกับ Deployment ใหม่เท่านั้น ต้อง Redeploy Project อีกครั้ง
Slide 10: ทดสอบ Production Flow
เปิด Production URL แล้วทดสอบตามลำดับ:
- เปิด
/issuesและเห็นรายการจาก Supabase - เปิดรายละเอียด Issue
- สร้าง Issue ใหม่จาก
/issues/new - กลับมาหน้ารายการและเห็นข้อมูลใหม่
- เปลี่ยน Status แล้ว Refresh
- ตรวจข้อมูลใน Supabase Table Editor
หน้าแรกเปิดได้ยังไม่ถือว่า Deploy สำเร็จ ต้องทดสอบ Read, Create และ Update ครบ
ถ้า Local ใช้ได้แต่ Production ไม่ได้
- ตรวจชื่อและค่าของ Environment Variables ใน Vercel
- Redeploy หลังเพิ่มหรือแก้ Variables
- อ่าน Build Logs และ Runtime Logs
- ตรวจว่า Vercel Deploy Repository และ Branch ที่ถูกต้อง
- ตรวจว่า URL กับ Key มาจาก Supabase Project เดียวกัน
- ตรวจ RLS Policies และ Grants
แก้ปัญหาในเครื่องและรัน npm run build ให้ผ่านก่อน Push เพื่อสร้าง Deployment ใหม่
Slide 11: สรุป Day 4
สิ่งที่ระบบทำได้แล้ว
- อ่าน List และ Detail จาก Supabase
- สร้าง Issue ด้วย Server Action
- Update Status และ
updated_at - ปิดงานด้วย Status
DONE - Deploy Next.js พร้อม Environment Variables ไปยัง Vercel
ข้อจำกัดที่ยังเหลือ
- ระบบยังไม่รู้ว่าใครกำลังใช้งาน
- Demo Policy ยังอนุญาตทั้ง
anonและauthenticated - ผู้ใช้ทั่วไปยัง Update Issue ทุก Row ได้
- Server Action ยังไม่ได้ตรวจ Role
Day 5 จะเพิ่ม Authentication, Session, Role USER/ADMIN, Page Guard และ RLS ที่ตรวจสิทธิ์จริง
Day 4: แอปเชื่อม Database และ Deploy ได้
Day 5: แอปรู้จักผู้ใช้และจำกัดสิทธิ์ได้คำศัพท์สำคัญ
| คำศัพท์ | ความหมาย |
|---|---|
| Update | แก้ไขข้อมูลใน Row ที่มีอยู่ |
| Close | ปิดงานด้วย Status โดยยังเก็บประวัติ |
| Soft Delete | ซ่อนข้อมูลด้วย Flag หรือเวลา แต่ยังเก็บ Row |
| Hard Delete | ลบ Row ออกจาก Database |
| Production Build | Build สำหรับตรวจและนำแอปไปใช้งานจริง |
| Environment Variables | ค่าตั้งค่าที่แยกออกจาก Source Code |
| Deployment | แอปเวอร์ชันหนึ่งที่ถูก Build และเปิดใช้งานบน Hosting |