[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"academy-blogs-th-1-1-all-push-notifications-websocket-chat-all--*":3,"academy-blog-translations-gw6n09i49lxlddz":100},{"data":4,"page":87,"perPage":87,"totalItems":87,"totalPages":87},[5],{"alt":6,"collectionId":7,"collectionName":8,"content":9,"cover_image":10,"cover_image_path":11,"created":12,"created_by":13,"expand":14,"id":95,"keywords":96,"locale":69,"published_at":97,"scheduled_at":13,"school_blog":91,"short_description":98,"status":89,"title":6,"updated":99,"updated_by":13,"slug":92,"views":94},"EP.38 การเพิ่มระบบ Push Notification ให้กับ WebSocket Chat","sclblg987654321","school_blog_translations","\u003Ch3 data-pm-slice=\"1 1 []\">\u003Cspan>\u003Cstrong>ทำไมต้องใช้ Push Notification กับ WebSocket Chat?\u003C\u002Fstrong>\u003C\u002Fspan>\u003C\u002Fh3>\u003Cp>\u003Cspan>แม้ว่า \u003Cstrong>WebSocket\u003C\u002Fstrong> จะช่วยให้แอปพลิเคชันสามารถรับส่งข้อมูลแบบเรียลไทม์ได้ แต่หากผู้ใช้ \u003Cstrong>ปิดแอปพลิเคชัน\u003C\u002Fstrong> หรือ \u003Cstrong>ขาดการเชื่อมต่อ\u003C\u002Fstrong> ก็จะไม่ได้รับข้อความใหม่ \u003Cstrong>Push Notification\u003C\u002Fstrong> ช่วยให้ผู้ใช้ยังคงได้รับการแจ้งเตือนแม้จะไม่ได้ใช้งานแอปอยู่\u003C\u002Fspan>\u003C\u002Fp>\u003Ch3>\u003Cspan>\u003Cstrong>เทคโนโลยีที่ใช้ในระบบแจ้งเตือน\u003C\u002Fstrong>\u003C\u002Fspan>\u003C\u002Fh3>\u003Col data-spread=\"false\">\u003Cli>\u003Cspan>\u003Cstrong>WebSocket Server\u003C\u002Fstrong> - ใช้สำหรับส่งข้อความระหว่างผู้ใช้\u003C\u002Fspan>\u003C\u002Fli>\u003Cli>\u003Cspan>\u003Cstrong>Web Push API\u003C\u002Fstrong> - ใช้สำหรับส่งการแจ้งเตือนไปยังเบราว์เซอร์ของผู้ใช้\u003C\u002Fspan>\u003C\u002Fli>\u003Cli>\u003Cspan>\u003Cstrong>Firebase Cloud Messaging (FCM)\u003C\u002Fstrong> - ใช้เป็น Notification Service สำหรับการส่งข้อความไปยังอุปกรณ์มือถือ\u003C\u002Fspan>\u003C\u002Fli>\u003Cli>\u003Cspan>\u003Cstrong>Database (PostgreSQL \u002F MongoDB)\u003C\u002Fstrong> - ใช้เก็บข้อมูลการสมัครรับแจ้งเตือนของผู้ใช้\u003C\u002Fspan>\u003C\u002Fli>\u003C\u002Fol>\u003Ch3>\u003Cspan>\u003Cstrong>ติดตั้งไลบรารีที่จำเป็น\u003C\u002Fstrong>\u003C\u002Fspan>\u003C\u002Fh3>\u003Cpre>\u003Ccode class=\"language-plaintext\">go get github.com\u002Fappleboy\u002Fgo-fcm\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch3 data-pm-slice=\"1 1 []\">\u003Cspan>\u003Cstrong>การตั้งค่าฐานข้อมูลเพื่อเก็บ Token ของผู้ใช้\u003C\u002Fstrong>\u003C\u002Fspan>\u003C\u002Fh3>\u003Cp>\u003Cspan>ไฟล์ \u003C\u002Fspan>\u003Ccode>\u003Cspan>schema.sql\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-plaintext\">CREATE TABLE notification_tokens (\n    id SERIAL PRIMARY KEY,\n    user_id INTEGER NOT NULL,\n    token TEXT NOT NULL UNIQUE\n);\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch3 data-pm-slice=\"1 1 []\">\u003Cspan>\u003Cstrong>การสร้าง GraphQL Schema สำหรับการสมัครรับแจ้งเตือน\u003C\u002Fstrong>\u003C\u002Fspan>\u003C\u002Fh3>\u003Cp>\u003Cspan>ไฟล์ \u003C\u002Fspan>\u003Ccode>\u003Cspan>schema.graphql\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-plaintext\">type Mutation {\n  registerNotificationToken(userID: ID!, token: String!): String!\n  sendPushNotification(userID: ID!, message: String!): String!\n}\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch3 data-pm-slice=\"1 1 []\">\u003Cspan>\u003Cstrong>การสร้าง Resolver สำหรับ Push Notification\u003C\u002Fstrong>\u003C\u002Fspan>\u003C\u002Fh3>\u003Cp>\u003Cspan>ไฟล์ \u003C\u002Fspan>\u003Ccode>\u003Cspan>resolver.go\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-plaintext\">package main\n\nimport (\n    \"context\"\n    \"database\u002Fsql\"\n    \"fmt\"\n    \"github.com\u002Fappleboy\u002Fgo-fcm\"\n    _ \"github.com\u002Flib\u002Fpq\"\n)\n\ntype Resolver struct {\n    db *sql.DB\n}\n\nfunc (r *Resolver) Mutation_registerNotificationToken(ctx context.Context, userID int, token string) (string, error) {\n    _, err := r.db.Exec(\"INSERT INTO notification_tokens (user_id, token) VALUES ($1, $2) ON CONFLICT (token) DO NOTHING\", userID, token)\n    if err != nil {\n        return \"Failed to register token\", err\n    }\n    return \"Token registered successfully\", nil\n}\n\nfunc (r *Resolver) Mutation_sendPushNotification(ctx context.Context, userID int, message string) (string, error) {\n    var token string\n    err := r.db.QueryRow(\"SELECT token FROM notification_tokens WHERE user_id = $1\", userID).Scan(&amp;token)\n    if err != nil {\n        return \"User token not found\", err\n    }\n\n    data := &amp;fcm.Message{\n        To: token,\n        Notification: &amp;fcm.Notification{\n            Title: \"New Message\",\n            Body:  message,\n        },\n    }\n\n    client, err := fcm.NewClient(\"YOUR_FIREBASE_SERVER_KEY\")\n    if err != nil {\n        return \"Failed to initialize FCM client\", err\n    }\n    \n    _, err = client.Send(data)\n    if err != nil {\n        return \"Failed to send notification\", err\n    }\n    return \"Notification sent successfully\", nil\n}\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch3 data-pm-slice=\"1 1 []\">\u003Cspan>\u003Cstrong>การเชื่อมต่อ WebSocket และ Push Notification\u003C\u002Fstrong>\u003C\u002Fspan>\u003C\u002Fh3>\u003Cp>\u003Cspan>ไฟล์ \u003C\u002Fspan>\u003Ccode>\u003Cspan>websocket_server.go\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-plaintext\">package main\n\nimport (\n    \"fmt\"\n    \"net\u002Fhttp\"\n    \"github.com\u002Fgorilla\u002Fwebsocket\"\n)\n\nvar upgrader = websocket.Upgrader{\n    CheckOrigin: func(r *http.Request) bool { return true },\n}\n\nfunc handleWebSocket(w http.ResponseWriter, r *http.Request) {\n    conn, _ := upgrader.Upgrade(w, r, nil)\n    defer conn.Close()\n    fmt.Println(\"Client connected\")\n    \n    for {\n        _, msg, err := conn.ReadMessage()\n        if err != nil {\n            break\n        }\n        fmt.Println(\"Received message:\", string(msg))\n    }\n}\n\nfunc main() {\n    http.HandleFunc(\"\u002Fws\", handleWebSocket)\n    fmt.Println(\"WebSocket Server Running on Port 8080\")\n    http.ListenAndServe(\":8080\", nil)\n}\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch3 data-pm-slice=\"1 1 []\">\u003Cspan>\u003Cstrong>การสมัครรับแจ้งเตือนในฝั่ง Client\u003C\u002Fstrong>\u003C\u002Fspan>\u003C\u002Fh3>\u003Cp>\u003Cspan>ไฟล์ \u003C\u002Fspan>\u003Ccode>\u003Cspan>client.js\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-plaintext\">async function registerNotification() {\n    const registration = await navigator.serviceWorker.register(\"\u002Fservice-worker.js\");\n    const subscription = await registration.pushManager.subscribe({\n        userVisibleOnly: true,\n        applicationServerKey: \"YOUR_PUBLIC_VAPID_KEY\"\n    });\n    \n    fetch(\"\u002Fgraphql\", {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application\u002Fjson\" },\n        body: JSON.stringify({\n            query: `mutation { registerNotificationToken(userID: 1, token: \"${subscription.endpoint}\") }`\n        })\n    });\n}\n\nregisterNotification();\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch3 data-pm-slice=\"1 1 []\">\u003Cspan>\u003Cstrong>ท้าให้ลอง!\u003C\u002Fstrong>\u003C\u002Fspan>\u003C\u002Fh3>\u003Cp>\u003Cspan>ลองเพิ่ม \u003Cstrong>การแจ้งเตือนแบบกลุ่ม (Group Notifications)\u003C\u002Fstrong> เพื่อแจ้งให้ทุกคนในห้องแชทได้รับข้อความใหม่แม้จะไม่ได้เปิดแอปอยู่\u003C\u002Fspan>\u003C\u002Fp>\u003Chr>\u003Ch3>\u003Cspan>\u003Cstrong>EP ถัดไป\u003C\u002Fstrong>\u003C\u002Fspan>\u003C\u002Fh3>\u003Cp>\u003Cspan>ใน \u003Cstrong>EP.39\u003C\u002Fstrong>, เราจะเพิ่ม \u003Cstrong>ฟีเจอร์การส่งไฟล์ในแชท (File Upload in WebSocket Chat)\u003C\u002Fstrong> เพื่อให้ผู้ใช้สามารถแชร์รูปภาพและเอกสารได้ 🚀\u003C\u002Fspan>\u003C\u002Fp>","47_11zon_34uis645ce.webp","https:\u002F\u002Ftwsme-r2.tumwebsme.com\u002Fsclblg987654321\u002F9ue9oos4kb64mhy\u002F47_11zon_34uis645ce.webp","2026-03-04 08:51:13.948Z","",{"keywords":15,"locale":63,"school_blog":73},[16,23,28,33,38,43,48,53,58],{"collectionId":17,"collectionName":18,"created":19,"created_by":13,"id":20,"name":21,"updated":22,"updated_by":13},"sclkey987654321","school_keywords","2026-03-04 08:51:12.024Z","mfphds63msma911","Chat Alerts","2026-04-10 16:14:33.622Z",{"collectionId":17,"collectionName":18,"created":24,"created_by":13,"id":25,"name":26,"updated":27,"updated_by":13},"2026-03-04 08:48:07.088Z","brfbypclggbbkcx","WebSocket API","2026-04-10 16:13:40.594Z",{"collectionId":17,"collectionName":18,"created":29,"created_by":13,"id":30,"name":31,"updated":32,"updated_by":13},"2026-03-04 08:51:12.327Z","oe33epaljgufqzf","Web Push API","2026-04-10 16:14:33.793Z",{"collectionId":17,"collectionName":18,"created":34,"created_by":13,"id":35,"name":36,"updated":37,"updated_by":13},"2026-03-04 08:47:05.949Z","caufix9o52uw4bh","Real-Time Chat","2026-04-10 16:13:23.517Z",{"collectionId":17,"collectionName":18,"created":39,"created_by":13,"id":40,"name":41,"updated":42,"updated_by":13},"2026-03-04 08:20:14.253Z","ah6lvy4x8qe08l5","Golang","2026-04-10 16:07:26.172Z",{"collectionId":17,"collectionName":18,"created":44,"created_by":13,"id":45,"name":46,"updated":47,"updated_by":13},"2026-03-04 08:20:11.547Z","ey3puyme01a9bsw","Go","2026-04-10 16:07:25.893Z",{"collectionId":17,"collectionName":18,"created":49,"created_by":13,"id":50,"name":51,"updated":52,"updated_by":13},"2026-03-04 08:51:12.685Z","v6aqa9mkbj1i0fg","Firebase Cloud Messaging","2026-04-10 16:14:33.924Z",{"collectionId":17,"collectionName":18,"created":54,"created_by":13,"id":55,"name":56,"updated":57,"updated_by":13},"2026-03-04 08:34:00.920Z","ecac9y661or1xka","WebSocket","2026-04-10 16:08:05.227Z",{"collectionId":17,"collectionName":18,"created":59,"created_by":13,"id":60,"name":61,"updated":62,"updated_by":13},"2026-03-04 08:46:14.110Z","itovzjisctbn2ej","Push Notification","2026-04-10 16:13:10.376Z",{"code":64,"collectionId":65,"collectionName":66,"created":67,"flag":68,"id":69,"is_default":70,"label":71,"updated":72},"th","pbc_1989393366","locales","2026-01-22 10:59:55.832Z","twemoji:flag-thailand","s8wri3bt4vgg2ji",true,"Thai","2026-04-10 15:42:46.614Z",{"category":74,"collectionId":75,"collectionName":76,"created":13,"expand":77,"id":91,"slug":92,"updated":93,"views":94},"wqxt7ag2gn7xcmk","pbc_2105096300","school_blogs",{"category":78},{"blogIds":79,"collectionId":80,"collectionName":81,"created":82,"created_by":13,"id":74,"image":83,"image_alt":13,"image_path":84,"label":85,"name":86,"priority":87,"publish_at":88,"scheduled_at":13,"status":89,"updated":90,"updated_by":13},[],"sclcatblg987654321","school_category_blogs","2026-03-04 08:33:53.210Z","59ty92ns80w_15oc1implw.png","https:\u002F\u002Ftwsme-r2.tumwebsme.com\u002Fsclcatblg987654321\u002Fwqxt7ag2gn7xcmk\u002F59ty92ns80w_15oc1implw.png",{"en":86,"th":86},"Golang The Series",1,"2026-03-16 04:39:38.440Z","published","2026-04-25 02:32:15.470Z","gw6n09i49lxlddz","push-notifications-websocket-chat","2026-05-08 22:25:15.137Z",310,"9ue9oos4kb64mhy",[20,25,30,35,40,45,50,55,60],"2025-03-17 02:09:10.313Z","เรียนรู้วิธีเพิ่ม Push Notification ให้กับ WebSocket Chat โดยใช้ Web Push API และ Firebase Cloud Messaging (FCM) เพื่อแจ้งเตือนผู้ใช้เมื่อมีข้อความใหม่แม้ในขณะที่ไม่ได้เปิดแอปพลิเคชัน","2026-04-22 07:11:44.871Z",{"th":92,"en":92}]