FileEditViewTerminal
IT Issue Bootcamp - Visual Studio Code
day-4-hour-4.mdx

Day 4 / Hour 4

Deploy Checkpoint

Update with Server Actions, demo permissions, production checks, and Vercel deployment.

60 minutes
Learners have a deployed app with demo read, create, and update flows.

Deploy Checkpoint

Day 4 - ชั่วโมงที่ 4: Update Status และ Deploy

เป้าหมายของชั่วโมงนี้

หลังจบชั่วโมงนี้ ผู้เรียนจะสามารถ:

  1. เปลี่ยน Status ของ Issue ใน Supabase ได้
  2. ใช้ Server Action สำหรับ Update ข้อมูลได้
  3. อธิบายความต่างระหว่าง Close, Soft Delete และ Hard Delete ได้
  4. ตรวจ Project ก่อนนำขึ้น Production ได้
  5. 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 INSERT

Hour นี้เราจะทำให้ปุ่ม 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 เป็นค่าอื่น              → หยุดและแจ้ง Error

Action นี้ไม่ใช้ 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

  1. เปิดหน้า /issues
  2. เลือก Issue ที่ยังไม่เป็น DONE
  3. กด Status ใหม่ เช่น IN_PROGRESS
  4. ตรวจว่า Badge บนหน้า List เปลี่ยน
  5. Refresh หน้าและตรวจว่าค่าไม่ย้อนกลับ
  6. เปิด Supabase Table Editor แล้วตรวจ status และ updated_at
  7. เปลี่ยน Status เป็น DONE เพื่อทดสอบการปิดงาน

Flow ที่กำลังทดสอบ

กดปุ่ม
  → hidden inputs สร้าง FormData
  → updateIssueStatusAction()
  → updateIssueStatus()
  → Supabase UPDATE
  → revalidatePath("/issues")
  → หน้า List แสดงข้อมูลล่าสุด

ถ้า Update ไม่สำเร็จ

  • permission denied: ตรวจ GRANT UPDATE และ RLS Policy จาก Hour 1
  • Invalid update request: ตรวจชื่อ Hidden Input id และ 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 เท่านั้น ไม่เหมาะกับข้อมูลจริง

ระบบจริงต้องตรวจหลายชั้น

  1. Authentication ตรวจว่า Request มาจากใคร
  2. Server Action ตรวจว่า User มี Role ที่อนุญาตหรือไม่
  3. 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 build

Production 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 push

Vercel จะ Deploy จาก Code ที่อยู่ใน Git Repository จึงต้อง Push Commit ล่าสุดก่อน Import Project หรือรอ Deployment ใหม่


Slide 9: Import Project และ Deploy บน Vercel

  1. เปิด Vercel Dashboard แล้วเลือกสร้าง Project ใหม่
  2. เลือก Git Provider และ Import Repository ของ Project
  3. ตรวจ Framework Preset เป็น Next.js
  4. ตรวจ Root Directory ให้ตรงกับ Folder ที่มี package.json
  5. เพิ่ม Environment Variables สองค่า:
NEXT_PUBLIC_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY

คัดลอกค่าจาก .env.local โดยไม่ใส่เครื่องหมายคำพูดหรือช่องว่างเกินมา และเลือกให้ใช้กับ Environment Production

  1. กด Deploy และรอให้ Build สำเร็จ
  2. เปิด Production URL ที่ Vercel สร้างให้

ไฟล์ .env.local อยู่เฉพาะในเครื่องและไม่ได้ถูกส่งขึ้น Vercel จึงต้องตั้งค่า Variables ใน Project Settings แยกต่างหาก

ถ้าแก้ Environment Variables ภายหลัง ค่าใหม่จะมีผลกับ Deployment ใหม่เท่านั้น ต้อง Redeploy Project อีกครั้ง


Slide 10: ทดสอบ Production Flow

เปิด Production URL แล้วทดสอบตามลำดับ:

  1. เปิด /issues และเห็นรายการจาก Supabase
  2. เปิดรายละเอียด Issue
  3. สร้าง Issue ใหม่จาก /issues/new
  4. กลับมาหน้ารายการและเห็นข้อมูลใหม่
  5. เปลี่ยน Status แล้ว Refresh
  6. ตรวจข้อมูลใน 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 BuildBuild สำหรับตรวจและนำแอปไปใช้งานจริง
Environment Variablesค่าตั้งค่าที่แยกออกจาก Source Code
Deploymentแอปเวอร์ชันหนึ่งที่ถูก Build และเปิดใช้งานบน Hosting

อ้างอิง