คู่มือพัฒนา LINE MINI App ฉบับลงมือทำ
สร้างแอปจองคิวคลินิกตั้งแต่ศูนย์จนส่ง Service Message

ทูโทเรียลที่ทำตามได้จริงทีละขั้น สร้าง LINE MINI App สำหรับจองคิวคลินิก ตั้งแต่สร้าง channel ตั้งค่า scope เขียนโค้ด LIFF ยืนยัน ID token ฝั่งเซิร์ฟเวอร์ บันทึกคิวลงฐานข้อมูล ส่งใบยืนยันคิวกลับเข้าแชทด้วย Service Message ไปจนถึงชวนเพื่อนด้วย Share Target Picker และยื่นรีวิว

โดย ทีม Mekha Innovationอ่าน 24 นาที

LINE MINI App คือเว็บแอปที่รันอยู่ใน LINE ผ่านแพลตฟอร์ม LIFF (LINE Front-end Framework) ผู้ใช้เปิดใช้งานได้ทันทีโดยไม่ต้องติดตั้งแอปใหม่ ไม่ต้องสมัครสมาชิกซ้ำ และระบุตัวตนได้จากบัญชี LINE ที่ใช้อยู่แล้ว สำหรับธุรกิจไทยที่มีฐานลูกค้าอยู่บน LINE เป็นหลัก นี่คือช่องทางที่ลด friction ได้มากที่สุดช่องทางหนึ่ง

บทความนี้ไม่ใช่คู่มือภาพรวม แต่เป็นทูโทเรียลที่ทำตามได้จริงตั้งแต่ต้นจนจบ โดยใช้ตัวอย่างเดียวตลอดบทความคือ “แอปจองคิวคลินิก” เนื้อหาอ้างอิงจาก LINE Codelabs สามชุด ได้แก่ Getting Started with LINE MINI App, Service Messages และ Vibe Coding LINE MINI App แล้วเรียบเรียงใหม่ให้ต่อกันเป็นโปรเจกต์เดียว

💡 อ่านจบแล้วคุณจะได้อะไร

MINI App จองคิวคลินิกที่ผู้ใช้เปิดจากแชท LINE, ล็อกอินด้วยบัญชี LINE, เลือกแพทย์และช่วงเวลา, ได้หมายเลขคิวที่บันทึกลงฐานข้อมูลจริง, ได้รับใบยืนยันคิวกลับเข้าแชทด้วย Service Message โดยไม่ต้องแอดเพื่อนกับ OA และแชร์แอปให้คนอื่นผ่าน Share Target Picker

สิ่งที่ต้องเตรียมรายละเอียด
บัญชี LINE Developersสมัครฟรีที่ developers.line.biz/console ด้วยบัญชี LINE ปกติ
เว็บโฮสต์ที่เป็น HTTPSendpoint ต้องเข้าถึงได้จริงจากภายนอก ใช้ localhost ไม่ได้ (StackBlitz, Vercel, Cloudflare หรือโฮสต์ของคุณเอง)
Node.js 18 ขึ้นไปสำหรับ backend ที่ยืนยัน token และเรียก Service Message API
ฐานข้อมูลอย่างง่ายเก็บตารางคิว จะเป็น Postgres, MySQL หรือ Firestore ก็ได้
โทรศัพท์จริง 1 เครื่องต้องทดสอบใน LINE app จริงทั้ง iOS และ Android ไม่ใช่แค่ responsive mode

1เข้าใจสถาปัตยกรรมและวางขอบเขตแอปจองคิวคลินิก

LINE MINI App ไม่ใช่แอปเนทีฟ แต่คือเว็บแอปของคุณเองที่ถูกเปิดผ่าน in-app browser ของ LINE โดยมี LIFF SDK เป็นสะพานเชื่อมระหว่างหน้าเว็บกับความสามารถของ LINE เช่น การรู้ว่าใครคือผู้ใช้ที่กำลังเปิดอยู่ การส่งข้อความกลับเข้าห้องแชท และการสแกน QR

  • Frontend: เว็บแอป (React/Vue/หน้าเว็บธรรมดา) ที่โฮสต์บน HTTPS และเรียก LIFF SDK ตอนเริ่มทำงาน
  • LIFF endpoint: URL ที่ผูกไว้กับ MINI App channel เป็นจุดที่ LINE จะเปิดให้ผู้ใช้
  • Backend: API ของคุณเอง ทำหน้าที่ยืนยัน ID token, ออกหมายเลขคิว, และเรียก Service Message API
  • ฐานข้อมูลคิว: เก็บว่าใครจองแพทย์คนไหน ช่วงเวลาใด และสถานะคิวเป็นอย่างไร
  • LINE MINI App Notice: ช่องทางที่ Service Message ถูกส่งไปหาผู้ใช้ โดยผู้ใช้ไม่ต้องแอดเพื่อนกับ OA ของคลินิก
flow ของแอปจองคิวคลินิก
ผู้ใช้แตะลิงก์ MINI App ในแชท
        |
        v
[LIFF] liff.init() -> ตรวจ login -> ได้ ID token
        |
        v
[Frontend] เลือกแพทย์ -> เลือกช่วงเวลา -> กดยืนยัน
        |
        v
[Backend] POST /api/bookings
        |-- ยืนยัน ID token กับ api.line.me/oauth2/v2.1/verify
        |-- กันจองซ้ำ + ออกหมายเลขคิว -> บันทึกลง DB
        |-- ส่ง Service Message ยืนยันคิว
        v
[LINE] ผู้ใช้ได้รับใบยืนยันคิวในแชททันที

💡 หลักคิดสำคัญ

ถ้าเว็บแอปของคุณทำงานได้ดีอยู่แล้วบนมือถือ การทำเป็น LINE MINI App คือการเพิ่มชั้นการระบุตัวตนและการเข้าถึง ไม่ใช่การเขียนระบบใหม่ทั้งหมด งานส่วนใหญ่จึงอยู่ที่ flow และการเชื่อมข้อมูล ไม่ใช่การรื้อ frontend

2สร้าง Provider และ MINI App channel

เข้า LINE Developers Console ที่ developers.line.biz/console แล้วสร้าง Provider ก่อน จากนั้นจึงสร้าง channel ประเภท LINE MINI App ใต้ Provider นั้น ชื่อ Provider คือชื่อที่ผู้ใช้จะเห็นตอนหน้าจอขอความยินยอม จึงควรใช้ชื่อองค์กรจริง

  1. 1

    สร้าง Provider

    กด Create a new provider ใช้ชื่อองค์กรจริง เช่น ชื่อคลินิกหรือชื่อบริษัทที่จดทะเบียน

  2. 2

    สร้าง LINE MINI App channel

    กด Create a new channel แล้วเลือกประเภท LINE MINI App กรอกชื่อแอป (เช่น Clinic Queue), คำอธิบาย, ไอคอน และเลือก category ให้ตรงกับธุรกิจ เช่น Medical/Healthcare

  3. 3

    ใส่ Endpoint URL

    ไปที่แท็บ Web app settings แล้วกรอก Endpoint URL (Developing) เป็น URL เว็บแอปของคุณ ต้องขึ้นต้นด้วย https:// และเปิดจากอินเทอร์เน็ตได้จริง

  4. 4

    จด LIFF ID ไว้

    ในหน้าเดียวกันจะมี LIFF URL (Developing) รูปแบบ https://miniapp.line.me/{LIFF_ID} ส่วนที่ตามหลัง miniapp.line.me/ คือ LIFF ID ที่ต้องใช้ในโค้ด

  5. 5

    แยก dev กับ production

    Console มีค่า Developing และ Published แยกกัน ให้ตั้ง endpoint คนละตัว แล้วสลับ LIFF ID ด้วย environment variable ไม่ใช่แก้โค้ดตอน deploy

⚠️ ทดสอบครั้งแรกต้องเปิดจากแชท

วิธีทดสอบที่ถูกต้องคือส่งลิงก์ LIFF URL เข้าห้องแชทของตัวเอง แล้วแตะเปิดจากในแอป LINE ระบบจะแสดงหน้าขอความยินยอมครั้งแรกครั้งเดียว ถ้าเปิดจาก address bar ของเบราว์เซอร์ผลลัพธ์จะต่างออกไป

3ตั้งค่า scope และสวิตช์ที่ต้องเปิดใน Console

ฟีเจอร์ของ LIFF แต่ละตัวจะใช้ได้ก็ต่อเมื่อเปิดสวิตช์หรือขอ scope ที่ตรงกันไว้ก่อน จุดที่ทำให้หลายทีมเสียเวลาคือ ค่าเหล่านี้อยู่คนละแท็บกัน บางอย่างอยู่ Basic settings บางอย่างอยู่ Web app settings

สิ่งที่อยากทำต้องตั้งค่าอะไรอยู่ที่ไหน
รู้ว่าใครกำลังเปิดแอปscope profile และ openidWeb app settings → Scopes
ดึงอีเมลผู้ใช้Email address permission ต้องกด Apply พร้อมแนบภาพอธิบายเหตุผลการใช้งาน แล้วรออนุมัติBasic settings
ส่งข้อความเข้าห้องแชทด้วย liff.sendMessages()scope chat_message.writeWeb app settings → Scopes
ให้ผู้ใช้แชร์แอปต่อเปิด toggle shareTargetPickerWeb app settings
สแกน QR รับผู้ป่วยหน้าเคาน์เตอร์เปิด toggle Scan QRWeb app settings
ชวนแอดเพื่อน OA ของคลินิกผูก Linked LINE Official Account แล้วตั้ง Add friend option เป็น On (Normal)Basic settings + Web app settings

⚠️ ขอ scope เท่าที่ใช้จริง

แอปจองคิวส่วนใหญ่ต้องการแค่ profile และ openid ก็ทำงานได้ครบ การขอ email หรือ scope อื่นเกินความจำเป็นทำให้ผู้ใช้ลังเลตอนกดยินยอม และเป็นเหตุผลที่ถูกตีกลับตอนยื่นรีวิวบ่อยมาก

4โค้ดชุดแรก: เริ่มต้น LIFF ในแอปคลินิก

ติดตั้ง SDK ด้วย npm install @line/liff (หรือแทรก script tag https://static.line-scdn.net/liff/edge/2/sdk.js ในหน้า HTML) จากนั้นเรียก liff.init() ให้เสร็จก่อนเรนเดอร์เนื้อหาที่ต้องใช้ข้อมูลผู้ใช้ กฎสำคัญคืออย่าเรียก liff.login() อัตโนมัติตอนโหลดหน้า ให้ผู้ใช้กดปุ่มเองเสมอ

src/lib/liff-client.ts
import liff from "@line/liff";

export type ClinicSession = {
  userId: string;
  displayName: string;
  pictureUrl?: string;
};

export async function initLiff() {
  await liff.init({
    liffId: import.meta.env.VITE_LIFF_ID,
    withLoginOnExternalBrowser: true,
  });

  // เปิดจากเบราว์เซอร์ปกติและยังไม่ล็อกอิน -> ให้ UI แสดงปุ่ม "เข้าสู่ระบบด้วย LINE"
  // อย่าเรียก liff.login() เองตอนโหลดหน้า ผู้ใช้จะงงและ redirect วนได้
  if (!liff.isLoggedIn()) {
    return { status: "guest" as const };
  }

  const idToken = liff.getIDToken();
  const res = await fetch("/api/session", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ idToken }),
  });

  if (!res.ok) throw new Error("verify failed");

  const session = (await res.json()) as ClinicSession;
  return { status: "signed-in" as const, session };
}

export function loginWithLine() {
  liff.login({ redirectUri: window.location.href });
}

export function logoutFromLine() {
  liff.logout();
  window.location.reload();
}

// ใช้ตัดสินใจว่าจะโชว์ปุ่มปิดหน้าต่างหรือปุ่มออกจากระบบ
export const runningInLineApp = () => liff.isInClient();
export const currentOs = () => liff.getOS(); // "ios" | "android" | "web"
  • withLoginOnExternalBrowser: true ทำให้เปิดจาก Chrome/Safari แล้วยังล็อกอินได้ ไม่ต้องเขียน flow แยก
  • ใน LINE app (liff.isInClient() เป็น true) ให้ซ่อนปุ่ม Login/Logout เพราะผู้ใช้ล็อกอินอยู่แล้ว
  • หลังจองเสร็จใน LINE app ใช้ liff.closeWindow() ปิดหน้าต่างกลับเข้าแชทให้อัตโนมัติ
  • ถ้าต้องเปิดลิงก์ภายนอก เช่น แผนที่คลินิก ใช้ liff.openWindow({ url, external: true })

⚠️ อย่าใช้ userId จากฝั่ง client เป็นตัวยืนยันตัวตน

liff.getProfile() สะดวกก็จริง แต่ข้อมูลที่ส่งมาจากเบราว์เซอร์ปลอมได้ ให้ยืนยัน ID token ฝั่งเซิร์ฟเวอร์ทุกครั้งก่อนผูกข้อมูลกับคนไข้หรืออนุมัติการจองใด ๆ

5ยืนยัน ID token ฝั่งเซิร์ฟเวอร์

LINE มี endpoint สำหรับตรวจสอบ ID token ให้โดยตรง ฝั่ง backend ควรเรียกใช้ทุกครั้งที่เริ่ม session ใหม่ แล้วจึงออก session ของระบบคุณเอง (cookie หรือ JWT) เพื่อไม่ต้องยิงตรวจซ้ำทุก request

server/verify.ts
export async function verifyIdToken(idToken: string) {
  const body = new URLSearchParams({
    id_token: idToken,
    client_id: process.env.LINE_CHANNEL_ID!,
  });

  const res = await fetch("https://api.line.me/oauth2/v2.1/verify", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body,
  });

  if (!res.ok) throw new Error("invalid id token");

  const claims = await res.json();
  // { sub, name, picture, email?, aud, exp, iss }

  if (claims.aud !== process.env.LINE_CHANNEL_ID) throw new Error("aud mismatch");
  if (claims.exp * 1000 < Date.now()) throw new Error("token expired");

  return {
    userId: claims.sub as string,
    displayName: claims.name as string,
    pictureUrl: claims.picture as string | undefined,
  };
}
  • ตรวจว่า aud ตรงกับ channel ID ของคุณ ป้องกัน token ที่ออกให้ channel อื่น
  • ตรวจ exp ว่ายังไม่หมดอายุ
  • ใช้ sub (LINE user ID) เป็นกุญแจผูกกับคนไข้ในฐานข้อมูล
  • เก็บ Channel Secret และ Channel ID ไว้ฝั่งเซิร์ฟเวอร์เท่านั้น อย่าฝังใน bundle ของ frontend

💡 ID token กับ access token คนละตัวกัน

ID token (liff.getIDToken()) ใช้พิสูจน์ว่าใครเป็นใคร ส่วน access token (liff.getAccessToken()) คือสิ่งที่ต้องส่งให้ backend เพื่อขอ notification token ตอนส่ง Service Message ในขั้นที่ 7 อย่าสลับสองตัวนี้กัน

6หน้าจองคิวและ API บันทึกคิว

ฝั่งหน้าจอทำให้เรียบที่สุด: เลือกแพทย์ เลือกช่วงเวลา ยืนยัน จบใน 3 แตะ ส่วนตรรกะทั้งหมด เช่น การกันจองซ้ำและการออกหมายเลขคิว ต้องอยู่ที่ backend เท่านั้น เพราะหน้าจอฝั่งผู้ใช้แก้ค่าได้เสมอ

schema ตารางคิว (PostgreSQL)
create table bookings (
  id            uuid primary key default gen_random_uuid(),
  line_user_id  text not null,
  display_name  text not null,
  doctor_id     text not null,
  slot_start    timestamptz not null,
  queue_number  int not null,
  status        text not null default 'confirmed', -- confirmed | cancelled | done
  created_at    timestamptz not null default now(),
  unique (doctor_id, slot_start)
);

create index bookings_user_idx on bookings (line_user_id, slot_start desc);
server/routes/bookings.ts
import { verifyIdToken } from "../verify";
import { sendQueueConfirmation } from "../service-message";
import { db } from "../db";

export async function createBooking(req: Request) {
  const { idToken, liffAccessToken, doctorId, slotStart } = await req.json();

  // 1) ใครเป็นคนจอง
  const user = await verifyIdToken(idToken);

  // 2) กันจองซ้ำช่วงเวลาเดียวกัน (unique constraint ช่วยอีกชั้น)
  const taken = await db.bookings.findBySlot(doctorId, slotStart);
  if (taken) {
    return Response.json({ error: "ช่วงเวลานี้ถูกจองแล้ว" }, { status: 409 });
  }

  // 3) ออกหมายเลขคิวของวันนั้น
  const queueNumber = await db.bookings.nextQueueNumber(doctorId, slotStart);

  const booking = await db.bookings.insert({
    lineUserId: user.userId,
    displayName: user.displayName,
    doctorId,
    slotStart,
    queueNumber,
  });

  // 4) ส่งใบยืนยันคิวกลับเข้าแชท (ขั้นที่ 7)
  await sendQueueConfirmation({ liffAccessToken, queueNumber });

  return Response.json({ queueNumber: booking.queueNumber });
}

💡 ทำไมต้องมี unique (doctor_id, slot_start)

การเช็กก่อน insert อย่างเดียวไม่พอ เพราะคนไข้สองคนอาจกดยืนยันพร้อมกันในเสี้ยววินาทีเดียว unique constraint คือด่านสุดท้ายที่การันตีว่าคิวจะไม่ซ้ำ

7ส่งใบยืนยันคิวกลับเข้าแชทด้วย Service Message

Service Message คือข้อความแจ้งเตือนที่ MINI App ส่งหาผู้ใช้ได้โดยผู้ใช้ไม่ต้องแอดเพื่อนกับ LINE Official Account ของคุณ ข้อความจะมาจากบัญชีระบบชื่อ “LINE MINI App Notice” เหมาะกับใบยืนยันคิว การเตือนก่อนถึงเวลานัด และการแจ้งเปลี่ยนแปลงนัดหมาย

7.1 เพิ่ม template ใน Console

  1. 1

    เปิดแท็บ Service message template

    ในหน้า channel ของคุณ แล้วกดปุ่ม Add

  2. 2

    เลือก Category และภาษา

    สำหรับการจองคิวให้เลือก Category เป็น Book และ Language เป็น Thai

  3. 3

    เลือกรูปแบบ template

    เช่น Booking confirmed (simple) ระบบจะสร้างชื่อสำหรับเรียกผ่าน API ให้ เช่น book_request_s_b_th ให้จดชื่อนี้ไว้ใช้ในโค้ด

  4. 4

    ทดสอบตัวแปรก่อนใช้จริง

    ในหน้าเดียวกันจะส่งทดสอบได้ด้วย JSON เช่น {"number":"A017","btn1_url":"https://line.me"} เพื่อดูหน้าตาข้อความจริงในแชทของคุณ

  5. 5

    ระบุ use case description

    อธิบายว่าจะใช้ส่งอะไร เช่น ยืนยันการจองคิวพบแพทย์ แล้วกด Add

7.2 โค้ดฝั่งเซิร์ฟเวอร์: สาม endpoint ที่ต้องเรียกตามลำดับ

การส่ง Service Message หนึ่งครั้งต้องเรียก API สามตัวต่อกัน คือขอ channel access token แบบ stateless ก่อน แล้วเอา LIFF access token ของผู้ใช้ไปแลก notification token สุดท้ายจึงส่งข้อความจริง

server/service-message.ts
const TOKEN_API = "https://api.line.me/oauth2/v3/token";
const NOTIFIER_API = "https://api.line.me/message/v3/notifier";
const TEMPLATE_NAME = "book_request_s_b_th";
const MINI_APP_URL = "https://miniapp.line.me/" + process.env.LIFF_ID;

// (1) ขอ stateless channel access token ด้วย client_credentials
async function issueChannelAccessToken() {
  const res = await fetch(TOKEN_API, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "client_credentials",
      client_id: process.env.LINE_CHANNEL_ID!,
      client_secret: process.env.LINE_CHANNEL_SECRET!,
    }),
  });
  if (!res.ok) throw new Error("token error: " + (await res.text()));
  const data = await res.json();
  return data.access_token as string;
}

// (2) แลก LIFF access token ของผู้ใช้เป็น notification token
async function issueNotificationToken(channelAccessToken: string, liffAccessToken: string) {
  const res = await fetch(NOTIFIER_API + "/token", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer " + channelAccessToken,
    },
    body: JSON.stringify({ liffAccessToken }),
  });
  if (!res.ok) throw new Error("notifier token error: " + (await res.text()));
  // { notificationToken, remainingCount, expiresIn, sessionId }
  const data = await res.json();
  return data.notificationToken as string;
}

// (3) ส่ง Service Message จริง
export async function sendQueueConfirmation(input: {
  liffAccessToken: string;
  queueNumber: number;
}) {
  const channelAccessToken = await issueChannelAccessToken();
  const notificationToken = await issueNotificationToken(
    channelAccessToken,
    input.liffAccessToken,
  );

  const res = await fetch(NOTIFIER_API + "/send?target=service", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer " + channelAccessToken,
    },
    body: JSON.stringify({
      templateName: TEMPLATE_NAME,
      params: {
        number: String(input.queueNumber),
        btn1_url: MINI_APP_URL,
        btn2_url: MINI_APP_URL,
        btn3_url: MINI_APP_URL,
        btn4_url: MINI_APP_URL,
      },
      notificationToken,
    }),
  });

  if (!res.ok) throw new Error("send error: " + (await res.text()));
}

7.3 ฝั่ง client ส่ง LIFF access token ไปด้วย

src/features/booking/submit.ts
import liff from "@line/liff";

export async function submitBooking(doctorId: string, slotStart: string) {
  const res = await fetch("/api/bookings", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      idToken: liff.getIDToken(),
      liffAccessToken: liff.getAccessToken(),
      doctorId,
      slotStart,
    }),
  });

  if (!res.ok) throw new Error((await res.json()).error ?? "จองไม่สำเร็จ");

  const { queueNumber } = await res.json();

  // อยู่ใน LINE app ก็ปิดหน้าต่างกลับเข้าแชทไปอ่านใบยืนยันคิวได้เลย
  if (liff.isInClient()) liff.closeWindow();

  return queueNumber as number;
}

⚠️ ข้อควรระวังของ Service Message

Channel Secret ต้องอยู่ฝั่งเซิร์ฟเวอร์เท่านั้น ห้ามหลุดไป frontend เด็ดขาด, notification token มีอายุจำกัด (expiresIn) และมีจำนวนครั้งที่ส่งได้ (remainingCount) ต่อหนึ่ง session จึงควรส่งทันทีหลังผู้ใช้ทำรายการ ไม่ใช่เก็บไว้ส่งทีหลัง และต้องส่งด้วย template ที่อนุมัติแล้วเท่านั้น ส่งข้อความอิสระไม่ได้

8ให้คนไข้ชวนกันเองด้วย Share Target Picker

เปิด toggle shareTargetPicker ใน Web app settings ก่อน แล้วจึงเพิ่มปุ่มแชร์ในแอป ปุ่มนี้ทำให้คนไข้ส่งการ์ดคลินิกพร้อมปุ่มจองคิวไปให้เพื่อนหรือกลุ่มครอบครัวได้ในแตะเดียว เป็นช่องทางเติบโตที่ไม่ต้องซื้อสื่อ

src/features/share/inviteFriends.ts
import liff from "@line/liff";

export async function inviteFriends() {
  if (!liff.isApiAvailable("shareTargetPicker")) return;

  await liff.shareTargetPicker([
    {
      type: "flex",
      altText: "จองคิวคลินิกผ่าน LINE ได้แล้ววันนี้",
      contents: {
        type: "bubble",
        hero: {
          type: "image",
          url: "https://your-domain.com/clinic-cover.jpg",
          size: "full",
          aspectRatio: "20:13",
          aspectMode: "cover",
        },
        body: {
          type: "box",
          layout: "vertical",
          contents: [
            { type: "text", text: "คลินิกสุขภาพดี", weight: "bold", size: "xl" },
            { type: "text", text: "จองคิวพบแพทย์ล่วงหน้า ไม่ต้องรอหน้าร้าน", wrap: true, size: "sm" },
          ],
        },
        footer: {
          type: "box",
          layout: "vertical",
          contents: [
            {
              type: "button",
              style: "primary",
              action: {
                type: "uri",
                label: "จองคิวเลย",
                uri: "https://miniapp.line.me/" + import.meta.env.VITE_LIFF_ID,
              },
            },
          ],
        },
      },
    },
  ]);
}
  • เช็ก liff.isApiAvailable("shareTargetPicker") เสมอ เพราะถ้ายังไม่เปิด toggle ใน Console จะเรียกไม่ได้
  • liff.sendMessages() ใช้ได้เฉพาะตอนเปิดในแอป LINE เท่านั้น ถ้าเปิดจากเบราว์เซอร์ภายนอกจะ error
  • ถ้าอยากชวนแอดเพื่อน OA ของคลินิกด้วย ใช้ liff.getFriendship() เช็ก friendFlag แล้วโชว์ลิงก์ https://line.me/R/ti/p/@your-oa-id เมื่อยังไม่เป็นเพื่อน

9ทางลัดสำหรับทีมที่ยังไม่มีนักพัฒนา: vibe coding

ถ้าอยากได้ต้นแบบเร็วภายในวันเดียว มีอีกเส้นทางที่ LINE เองก็ทำ codelab ไว้ คือให้ AI สร้างเว็บแอปจากคำสั่งภาษาคน แล้วค่อยเอา URL ไปผูกเป็น MINI App เส้นทางนี้เหมาะกับการทำ prototype ไปคุยกับทีมแพทย์และผู้บริหาร ก่อนตัดสินใจลงทุนพัฒนาเต็มรูปแบบ

  1. 1

    สร้างเว็บจองคิวด้วย AI

    ใช้เครื่องมืออย่าง Google AI Studio, Lovable หรือ Cursor สั่งสร้างหน้าจองคิวพร้อม API เก็บข้อมูล

  2. 2

    เปิดให้เข้าถึงแบบสาธารณะ

    ตั้งค่าแชร์เป็น public แล้วคัดลอก Shared URL ที่เป็น HTTPS ระวังอย่าใช้ URL สำหรับแก้ไขโปรเจกต์

  3. 3

    ผูกเป็น Endpoint URL

    นำ URL นั้นไปวางในช่อง Endpoint URL (Developing) ของ MINI App channel

  4. 4

    สั่ง AI ผสาน LIFF และ Service Message

    prompt รอบสองให้เพิ่ม @line/liff, ปุ่ม login/logout, โปรไฟล์ผู้ใช้ และ flow ส่ง Service Message สามขั้นตามขั้นที่ 7

ตัวอย่าง prompt ที่ใช้ได้จริง
เพิ่มการเชื่อมต่อ LINE MINI App เข้ากับเว็บจองคิวคลินิกนี้:

1. ติดตั้งและใช้แพ็กเกจ @line/liff
2. เรียก liff.init({ liffId: LIFF_ID, withLoginOnExternalBrowser: true })
   ตอนแอปเริ่มทำงาน ห้ามเรียก liff.login() อัตโนมัติ
3. แสดงปุ่ม "เข้าสู่ระบบด้วย LINE" และ "ออกจากระบบ"
   เฉพาะเมื่อ liff.isInClient() เป็น false
4. เมื่อผู้ใช้ล็อกอินแล้ว ใช้ liff.getProfile() แสดงการ์ดโปรไฟล์
   และเติมชื่อผู้จองในฟอร์มให้อัตโนมัติ
5. ตอนกดยืนยันการจอง ให้ส่ง liff.getIDToken() และ liff.getAccessToken()
   ไปที่ POST /api/bookings
6. ฝั่งเซิร์ฟเวอร์ใช้ LINE_CHANNEL_ID และ LINE_CHANNEL_SECRET จาก env
   เพื่อขอ channel access token, แลก notification token
   และส่ง Service Message template ชื่อ book_request_s_b_th
   โดยส่ง params.number เป็นหมายเลขคิว
7. อย่าเปิดเผย LINE_CHANNEL_SECRET ในโค้ดฝั่ง client เด็ดขาด

💡 prototype กับ production ไม่เหมือนกัน

เส้นทาง vibe coding ดีมากสำหรับพิสูจน์ไอเดียและเก็บ feedback แต่ก่อนใช้กับคนไข้จริงต้องย้ายไปโฮสต์ที่คุณควบคุมได้ มีฐานข้อมูลจริง มีการยืนยัน ID token ฝั่งเซิร์ฟเวอร์ และมีนโยบายความเป็นส่วนตัวที่ครอบคลุมข้อมูลสุขภาพ

10ทดสอบก่อนยื่นรีวิว

  • ทดสอบบนเครื่องจริงทั้ง iOS และ Android ไม่ใช่แค่ responsive mode ในเบราว์เซอร์
  • เปิดจากลิงก์ในห้องแชทจริง แล้วดูหน้าขอความยินยอมครั้งแรกว่าข้อความถูกต้อง
  • ทดสอบเปิดจากเบราว์เซอร์ภายนอกทั้งตอนล็อกอินและยังไม่ล็อกอิน
  • ทดสอบกรณีผู้ใช้กดปฏิเสธการให้สิทธิ์ ว่าแอปไม่ค้างและมีข้อความอธิบาย
  • ทดสอบจองซ้ำช่วงเวลาเดียวกันจากสองเครื่องพร้อมกัน ว่าได้ error 409 ไม่ใช่คิวซ้ำ
  • ทดสอบว่า Service Message เข้าจริงในแชท และปุ่มในข้อความเปิดกลับมาที่ MINI App ได้
  • ทดสอบเน็ตช้าและการปิดหน้ากลางคัน ว่าคิวไม่ถูกบันทึกครึ่งทาง
  • ตรวจว่าหน้าจอแรกโหลดเสร็จเร็ว เพราะผู้ใช้ LINE คาดหวังความไวระดับแชท
  • ตรวจข้อความขอความยินยอมและลิงก์ privacy policy ว่าเข้าถึงได้จริง
  • สลับ channel dev/production แล้วทดสอบซ้ำ ว่า endpoint และ LIFF ID ถูกต้องทั้งคู่

⚠️ เหตุผลที่ถูกตีกลับบ่อยตอนรีวิว

endpoint เข้าไม่ได้ในบางช่วง, ไม่มี privacy policy, ขอ scope เกินความจำเป็น, เนื้อหาไม่ตรงกับคำอธิบายแอป, ใช้ Service Message ผิดวัตถุประสงค์ไปเป็นข้อความโฆษณา และปุ่มสำคัญใช้งานไม่ได้บนอุปกรณ์จริง

11หลังเผยแพร่: วัดผลและดูแลต่อเนื่อง

หลังเปิดใช้งาน ให้ตั้งตัวชี้วัดตั้งแต่วันแรก อย่างน้อยควรรู้ว่ามีคนเปิดกี่คน เริ่มจองกี่คน จองสำเร็จกี่คน และหลุดที่ขั้นไหนมากที่สุด สำหรับคลินิก ตัวเลขที่ผู้บริหารสนใจที่สุดมักเป็นอัตราการไม่มาตามนัด (no-show) ที่ลดลงหลังเริ่มส่ง Service Message เตือนคิว

Booking completion

สัดส่วนผู้ที่เปิดแอปแล้วจองคิวสำเร็จ ถ้าต่ำมักแปลว่าฟอร์มยาวหรือช่วงเวลาว่างน้อยเกินไป

No-show rate

เทียบก่อนและหลังเริ่มส่ง Service Message เตือนคิวล่วงหน้า 1 วันและ 2 ชั่วโมง

Repeat booking

สัดส่วนคนไข้ที่กลับมาจองซ้ำภายใน 90 วัน สะท้อนว่าประสบการณ์ดีพอให้ใช้ต่อ

Staff time saved

จำนวนสายโทรและแชทที่เคาน์เตอร์ไม่ต้องรับเอง เพราะคนไข้จองเองได้

สุดท้าย อย่ามอง MINI App เป็นโปรเจกต์ที่จบในรอบเดียว ทีมที่ได้ผลลัพธ์ดีมักปล่อยเวอร์ชันแรกที่เล็กแต่ใช้ได้จริง เช่น จองคิวแพทย์คนเดียวก่อน แล้วค่อยขยายเป็นหลายสาขา เพิ่มประวัติการรักษา และเชื่อม HIS ในรอบถัดไป โดยปรับตามพฤติกรรมผู้ใช้ทุก 2-4 สัปดาห์

💡 ต้องการทีมที่ทำมาแล้วจริง

Mekha Innovation เป็น LINE API Expert ที่ออกแบบและพัฒนา LINE MINI App ให้ธุรกิจไทย ตั้งแต่วางขอบเขต เชื่อมระบบหลังบ้าน จนดูแลหลังเผยแพร่ ติดต่อทีมงานเพื่อประเมินโปรเจกต์ของคุณได้

หมวดหมู่อื่น