Search
๐Ÿ“š

EP25. Supabase Push Notification

์ƒ์„ฑ์ผ
2024/06/30 12:15
๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ
2024/07/01

EP25. Supabase Push Notification

EP24 ๊ฐ•์˜์—์„œ๋Š” Flutter์•ฑ์—์„œ FCM Token์„ ์–ป๊ณ  Firebase ์ฝ˜์†”์—์„œ ํ‘ธ์‹œ์•Œ๋ฆผ์„ ๋ฐœ์†กํ•˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.
์„œ๋น„์Šค ์šด์˜์—์„œ๋Š” ๋ฐฑ์•ค๋“œ ๋กœ์ง์— ์˜ํ•ด ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ๋ฐœ์†กํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
์˜ˆ์‹œ)
โ€ข
๋งค์ผ ์•„์นจ 9์‹œ ๋งˆ์ผ€ํŒ… ๊ด€๋ จ ๋ฉ”์‹œ์ง€
โ€ข
์ƒํ’ˆ ๋ฐฐ์†ก ์™„๋ฃŒ ์•Œ๋ฆผ
โ€ข
์ฑ„ํŒ…๋ฉ”์‹œ์ง€ ์ˆ˜์‹  ์•Œ๋ฆผ

1. ์‹œ์Šคํ…œ ๊ตฌ์กฐ๋„

Supabase Edge Functions ๋ฐฑ์•ค๋“œ ๋กœ์ง์—์„œ ์ง์ ‘์ ์œผ๋กœ Push๋ฉ”์‹œ์ง€๋ฅผ ๋ฐœ์†กํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. (๋ฌผ๋ก  ๊ธฐ์ˆ ์ ์œผ๋กœ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค๋งŒ ๋” ๋‚˜์€ ๋ฐฉ๋ฒ•์„ ์ œ์‹œํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.)
Supabase Edge Functions์— API์™ธ ํ‘ธ์‹œ ๋ฐœ์†ก์„ ์œ„ํ•œ Push ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
Supabase Database์— notification ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•˜๊ณ  row๊ฐ€ ์ถ”๊ฐ€๋˜๋Š” ๊ฒฝ์šฐ ํ‘ธ์‹œ ์•Œ๋ฆผ์ด ๋ฐœ์†ก๋˜๋„๋ก trigger๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

2. Firebase Admin ๊ถŒํ•œ ํš๋“

Push ๋ฐœ์†ก์€ Firebase Admin์„ ์ด์šฉํ•ด ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค. Supabase์—์„œ Firebase Admin๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” Firebase Admin Key๋ฅผ ์ด์šฉํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.
โ€ข
Firebase > ํ”„๋กœ์ ํŠธ ์„ค์ • > ์„œ๋น„์Šค ๊ณ„์ • > Firebase Admin SDK
`์ƒˆ ๋น„๊ณต๊ฐœ ํ‚ค ์ƒ์„ฑ`๋ฒ„ํŠผ์œผ๋กœ ๋น„๊ณต๊ฐœ key ํŒŒ์ผ์„ ๋‹ค์šด๋ฐ›์Šต๋‹ˆ๋‹ค. Admin SDK๊ตฌ์„ฑ ์Šค๋‹ˆํŽซ์€ Node.js๋ฅผ ์„ ํƒํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.
โ€ข
๋™์ผํ•œ Key๋Š” ๋‹จ 1๋ฒˆ๋งŒ ๋‹ค์šด๋ฐ›์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์•ˆ์ „ํ•œ ๊ณณ์— ์ €์žฅํ•ด๋‘์‹œ๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค.
โ€ข
์ด Key๋Š” Admin Key๋กœ ๋งค์šฐ ๊ฐ•๋ ฅํ•œ ๊ถŒํ•œ์„ ํ–‰์‚ฌํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ Public Github ๋“ฑ ๊ณต๊ฐœ๋œ ์žฅ์†Œ์— ๋ฐฉ์น˜๋˜์ง€ ์•Š๋„๋ก ์ฃผ์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
{firebase ํ”„๋กœ์ ํŠธ ๊ด€๋ จ๋œ ์ด๋ฆ„}.json ํŒŒ์ผ์ด ๋‹ค์šด๋กœ๋“œ ๋˜๋ฉฐ ํŒŒ์ผ์„ ์—ด์–ด๋ณด๋ฉด json ํ˜•ํƒœ๋กœ ์—ฌ๋Ÿฌ key๋“ค์ด ์ €์žฅ๋˜์–ด ์žˆ์Œ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

3. Firebase Admin Key ๋กœ์ปฌ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ •

2๋ฒˆ ๊ณผ์ •์—์„œ ์–ป์€ Firebase Admin Key๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜์ง€๋งŒ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
์œ„ ๊ฐ•์˜์—์„œ supabase๋””๋ ‰ํ† ๋ฆฌ ํ•˜์œ„์— .env.production ์ด๋ผ๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜ ํŒŒ์ผ์„ ์ƒ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
์ด ํ™˜๊ฒฝ๋ณ€์ˆ˜ ํŒŒ์ผ์— Firebase Admin Key์ •๋ณด๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
โ€ข
FB_CLIENT_EMAIL
โ—ฆ
client_email ๊ฐ’์„ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค
โ€ข
FB_PRIVATE_KEY
โ—ฆ
private_key ๊ฐ’์„ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค.
โ—ฆ
๊ฐœํ–‰๋ฌธ์ž, โ€œโ€”โ€”-BEGIN PRIVATE KEYโ€”โ€”-โ€ prefix, โ€œโ€”โ€”-END PRIVATE KEYโ€”โ€”-\nโ€ suffix๋ฅผ ๋ชจ๋‘ ํฌํ•จํ•˜๋„๋ก. ์›๋ณธ ๊ทธ๋Œ€๋กœ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค.
โ€ข
FB_PROJECT_ID
โ—ฆ
project_id ๊ฐ’์„ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค.
์ฐธ๊ณ ) ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ณ€๊ฒฝ ์‹œ supabase start ๋ช…๋ น์–ด๋กœ supabase๋ฅผ ์žฌ์‹œ์ž‘ ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

4. ๋กœ์ปฌ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ๋ฐฑ์•ค๋“œ ๋กœ์ง์—์„œ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋กœ์ง ๊ตฌํ˜„

supabase/functions/environments.ts ํŒŒ์ผ์— ์ถ”๊ฐ€ํ•œ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
โ€ข
supabase/functions/environments.ts
export const supabaseProjectURL = Deno.env.get( "SB_PROJECT_URL", )!; export const supabaseServiceRoleKey = Deno.env.get( "SB_SERVICE_ROLE_KEY", )!; export const firebaseClientEmail = Deno.env.get( "FB_CLIENT_EMAIL", )!; export const firebasePrivateKey = Deno.env.get( "FB_PRIVATE_KEY", )!.replace(/\\n/g, "\n"); // ๐Ÿ‘ˆ replace ๋กœ์ง ์ถ”๊ฐ€ ์ฃผ์˜ export const firebaseProjectID = Deno.env.get( "FB_PROJECT_ID", )!;
TypeScript
๋ณต์‚ฌ
์ถ”๊ฐ€ํ•œ Firebase ๊ด€๋ จ ํ™˜๊ฒฝ๋ณ€์ˆ˜ 3๊ฐ€์ง€๋ฅผ Deno Framework์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
Private Key์˜ ๊ฒฝ์šฐ ๊ฐœํ–‰๋ฌธ์ž ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด replace ๋กœ์ง์ด ํ•„์š”ํ•จ์— ์ฃผ์˜ํ•ฉ๋‹ˆ๋‹ค.

5. Firebase Admin Key Public ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ •

Public Supabase์—์„œ๋„ Firebase Admin Key๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
โ€ข
Supabase > Settings > Edge Functions

6. Notification ํ…Œ์ด๋ธ” ์ƒ์„ฑ

Notification Trigger๊ฐ€ ๋  Database Table์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
ํ…Œ์ด๋ธ”์˜ ์ด๋ฆ„์€ fcm_notifications๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. (๋‹ค๋ฅธ์ด๋ฆ„๋„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.)
Column ์„ค๋ช…
โ€ข
id
โ—ฆ
primary ๊ฐ’์œผ๋กœ auto increment๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
โ€ข
created_at
โ—ฆ
row๊ฐ€ ์ถ”๊ฐ€๋œ ์‹œ์ ์œผ๋กœ. ๋ฐฑ์•ค๋“œ ๋กœ์ง์œผ๋กœ๋ถ€ํ„ฐ Push ๋ฐœ์†ก ์•Œ๋ฆผ์„ ์š”์ฒญ๋ฐ›์€ ์‹œ๊ฐ„์ด ๋ฉ๋‹ˆ๋‹ค.
โ€ข
completed_at
โ—ฆ
FCM์— Push ๋ฐœ์†ก ์š”์ฒญ์„ ์™„๋ฃŒํ•œ ์‹œ์ ์ด ๋ฉ๋‹ˆ๋‹ค.
โ€ข
user_id
โ—ฆ
push๋ฅผ ์ˆ˜์‹ ํ•  user์˜ ID ๊ฐ’์ž…๋‹ˆ๋‹ค.
โ€ข
title
โ—ฆ
Push Notification์˜ ์ œ๋ชฉ์œผ๋กœ ๋ณด์—ฌ์งˆ ํ…์ŠคํŠธ์ž…๋‹ˆ๋‹ค
โ€ข
body
โ—ฆ
Push Notification์˜ ๋ณธ๋ฌธ์œผ๋กœ ๋ณด์—ฌ์งˆ ํ…์ŠคํŠธ์ž…๋‹ˆ๋‹ค
โ€ข
result
โ—ฆ
FCM์— Push ๋ฐœ์†ก ์š”์ฒญ์— ๋Œ€ํ•œ ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

7. Push ํ•จ์ˆ˜ ์ƒ์„ฑ

supabase functions new push
Bash
๋ณต์‚ฌ
์œ„ ๋ช…๋ น์–ด๋กœ push๋ผ๋Š” ์ด๋ฆ„์˜ ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
supabase/functions ํ•˜์œ„์— push ๋””๋ ‰ํ† ๋ฆฌ์™€ index.ts ํŒŒ์ผ์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.
โ€ข
supabase/functions/push/index.ts
import { firebaseClientEmail, firebasePrivateKey, firebaseProjectID, } from "../environments.ts"; import { JWT } from "npm:google-auth-library@9"; import { supabase } from "../api/lib/supabase.ts"; // fcm_notifications ํ…Œ์ด๋ธ”์˜ ๋ ˆ์ฝ”๋“œ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์ธํ„ฐํŽ˜์ด์Šค interface UserNotificationRecord { id: string; user_id: string; title: string | null; body: string | null; } // Webhook ํŽ˜์ด๋กœ๋“œ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์ธํ„ฐํŽ˜์ด์Šค interface UserNotificationWebhookPayload { type: "INSERT"; table: string; record: UserNotificationRecord; schema: "public"; } // ์บ์‹œ๋œ ์•ก์„ธ์Šค ํ† ํฐ ๋ฐ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ let cachedAccessToken: string | null = null; let tokenExpirationTime: number | null = null; // ์›นํ›… ์—”๋“œํฌ์ธํŠธ Deno.serve(async (req) => { const payload: UserNotificationWebhookPayload = await req.json(); // FCM ํ† ํฐ ์กฐํšŒ const { data, error } = await supabase .from("fcm_tokens") .select("fcm_token") .eq("user_id", payload.record.user_id); const completedAt = new Date().toISOString(); // FCM ํ† ํฐ์ด ์—†๊ฑฐ๋‚˜ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ if (error || !data || data.length === 0) { console.error("Error fetching FCM tokens:", error || "No FCM tokens found"); await updateNotificationResult( payload.record.id, completedAt, { "NOT_EXIST_USER": [] }, ).catch((err) => console.error("Error updating result for non-existent user:", err) ); return new Response( JSON.stringify({ error: "No FCM tokens found for the user" }), { headers: { "Content-Type": "application/json" }, status: 404, }, ); } const fcmTokens = data.map((row: { fcm_token: string }) => row.fcm_token); try { // FCM ์•Œ๋ฆผ์„ ๋ณด๋‚ด๊ธฐ ์œ„ํ•œ ์•ก์„ธ์Šค ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ const accessToken = await getAccessToken({ clientEmail: firebaseClientEmail, privateKey: firebasePrivateKey, }); // ๊ฐ FCM ํ† ํฐ์— ๋Œ€ํ•ด ์•Œ๋ฆผ ์ „์†ก const notificationPromises = fcmTokens.map((token) => sendNotification(token, payload.record, accessToken) ); // ๋ชจ๋“  ์•Œ๋ฆผ ์ „์†ก ์ž‘์—… ์™„๋ฃŒ ๋Œ€๊ธฐ const results = await Promise.all(notificationPromises); // ๊ฒฐ๊ณผ ์š”์•ฝ const resultSummary: { [key: string]: string[] } = { "SUCCESS": [] }; results.forEach(({ fcmToken, status }) => { if (status === "SUCCESS") { resultSummary["SUCCESS"].push(fcmToken); } else { if (!resultSummary[status]) { resultSummary[status] = []; } resultSummary[status].push(fcmToken); } }); // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๊ฒฐ๊ณผ ์—…๋ฐ์ดํŠธ await updateNotificationResult( payload.record.id, completedAt, resultSummary, ).catch((err) => console.error("Error updating notification result:", err)); return new Response(JSON.stringify({ results }), { headers: { "Content-Type": "application/json" }, }); } catch (err) { console.error("Error sending FCM messages:", err); await updateNotificationResult( payload.record.id, completedAt, { "SEND_ERROR": [] }, ).catch((err) => console.error("Error updating result for send error:", err) ); return new Response( JSON.stringify({ error: "Failed to send FCM messages" }), { headers: { "Content-Type": "application/json" }, status: 500, }, ); } }); // FCM ์•Œ๋ฆผ์„ ๋ณด๋‚ด๊ธฐ ์œ„ํ•œ ์•ก์„ธ์Šค ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ const getAccessToken = async ({ clientEmail, privateKey, }: { clientEmail: string; privateKey: string; }): Promise<string> => { const now = Date.now(); if (cachedAccessToken && tokenExpirationTime && now < tokenExpirationTime) { return cachedAccessToken; } const jwtClient = new JWT({ email: clientEmail, key: privateKey, scopes: ["https://www.googleapis.com/auth/firebase.messaging"], }); const tokens = await jwtClient.authorize(); cachedAccessToken = tokens.access_token!; // ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์„ค์ • tokenExpirationTime = tokens.expiry_date!; return cachedAccessToken; }; // ๋‹จ์ผ FCM ํ† ํฐ์— ์•Œ๋ฆผ ์ „์†ก const sendNotification = async ( fcmToken: string, record: UserNotificationRecord, accessToken: string, ): Promise<{ fcmToken: string; status: string }> => { try { const res = await fetch( `https://fcm.googleapis.com/v1/projects/${firebaseProjectID}/messages:send`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ message: { token: fcmToken, notification: { title: record.title, body: record.body, }, }, }), }, ); const resData = await res.json(); if (res.status < 200 || 299 < res.status) { const errorCode = resData.error?.details?.[0]?.errorCode || resData.error?.status || "UNKNOWN_ERROR"; console.error("Error sending FCM message:", errorCode); return { fcmToken, status: errorCode }; } else { return { fcmToken, status: "SUCCESS" }; } } catch (err) { console.error("Error sending FCM message:", err); return { fcmToken, status: "SEND_ERROR" }; } }; // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๊ฒฐ๊ณผ ์—…๋ฐ์ดํŠธ const updateNotificationResult = async ( id: string, completedAt: string, result: { [key: string]: string[] }, ) => { try { const { error } = await supabase .from("fcm_notifications") .update({ completed_at: completedAt, result: JSON.stringify(result) }) .eq("id", id); if (error) { console.error("Error updating notification result:", error); throw error; } } catch (err) { console.error("Exception updating notification result:", err); throw err; } };
TypeScript
๋ณต์‚ฌ

8. Push ํ•จ์ˆ˜ ๋ฐฐํฌ

bash deploy.sh
TypeScript
๋ณต์‚ฌ
deploy ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•˜์—ฌ push ํ•จ์ˆ˜๋ฅผ public์œผ๋กœ ๋ฐฐํฌํ•ฉ๋‹ˆ๋‹ค.
โ€ข
Supabase > Edge Functions
Edge Functions ๋Œ€์‹œ๋ณด๋“œ์—์„œ push ํ•จ์ˆ˜๊ฐ€ ์ถ”๊ฐ€๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

9. Supabase Database Webhook ์„ค์ •

fcm_notifications ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ๊ฐ€ ์ƒ์„ฑ๋˜๋ฉด Push ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก webhook์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
โ€ข
Supabase > Database > Webhooks
Enable Webhooks ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ Webhook์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
โ€ข
fcm_notifications ํ…Œ์ด๋ธ”์„ ํŠธ๋ฆฌ๊ฑฐ๋กœ ์‚ฌ์šฉํ•˜๊ฒ ๋‹ค๋Š” ์„ ์–ธ์ž…๋‹ˆ๋‹ค.
โ€ข
fcm_notifications ํ…Œ์ด๋ธ”์— ์ƒˆ๋กœ์šด row๊ฐ€ ์ถ”๊ฐ€๋˜๋Š” ๊ฒƒ์„ ํŠธ๋ฆฌ๊ฑฐ๋กœ ์‚ฌ์šฉํ•˜๊ฒ ๋‹ค๋Š” ์„ ์–ธ์ž…๋‹ˆ๋‹ค.
โ€ข
ํŠธ๋ฆฌ๊ฑฐ๊ฐ€ ๋˜๋ฉด Supabase Edge Function์„ ํ˜ธ์ถœํ•˜๊ฒ ๋‹ค๋Š” ์„ ์–ธ์ž…๋‹ˆ๋‹ค.
โ€ข
Edge Functions ์ค‘ push ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๊ฒ ๋‹ค๋Š” ์„ ์–ธ์ž…๋‹ˆ๋‹ค.

10. Supabase DB pull

Supabase์ฝ˜์†”์—์„œ fcm_notifications ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•˜๊ณ  webhook์„ ์ƒ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
์ž‘์—… ๋‚ด์šฉ์„ ๋กœ์ปฌ์—๋„ ๋ฐ˜์˜ํ•  ์ˆ˜ ์žˆ๋„๋ก ์•„๋ž˜ ๋ช…๋ น์–ด๋กœ DB ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ๋กœ์ปฌ๋กœ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
supabase db pull
Bash
๋ณต์‚ฌ
supabase/migrations ๋””๋ ‰ํ† ๋ฆฌ ํ•˜์œ„์— DB ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ํŒŒ์ผ๋กœ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. (์ƒํ™ฉ์— ๋”ฐ๋ผ sqlํŒŒ์ผ ๊ฐฏ์ˆ˜๋Š” ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.)

11. Database๋ฅผ ํ™œ์šฉํ•œ Push ๋ฐœ์†ก ํ…Œ์ŠคํŠธ

โ€ข
supabase > database > fcm_notifications table
insert row ๋ฉ”๋‰ด๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— Row๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
fcm_tokens ํ…Œ์ด๋ธ”์— ๋“ฑ๋ก๋œ, fcm_token์„ ๋ณด์œ ํ•œ user_id์™€ ํ‘ธ์‹œ ์•Œ๋ฆผ title, body์ •๋ณด๋ฅผ ์ž…๋ ฅํ•˜๊ณ  Save๋ฒ„ํŠผ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
ํŠธ๋ฆฌ๊ฑฐ์— ์˜ํ•ด Push ๋ฉ”์‹œ์ง€๊ฐ€ ๋ฐœ์†ก๋˜์–ด ๋””๋ฐ”์ด์Šค์—์„œ Push ์•Œ๋ฆผ ์ˆ˜์‹ ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
fcm_notifications ํ…Œ์ด๋ธ”์—์„œ ํ‘ธ์‹œ ๋ฐœ์†ก ์™„๋ฃŒ ์‹œ๊ฐ„๊ณผ FCM token๋ณ„ ์„ฑ๊ณต ์—ฌ๋ถ€ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

12. ๋ฐฑ์•ค๋“œ ๋กœ์ง์—์„œ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š” ํ•จ์ˆ˜๋กœ ๊ตฌํ˜„

fcm_notification ํ…Œ์ด๋ธ”์— user_id, title, body๋ฅผ insertํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด ๋ฐฑ์•ค๋“œ ๋กœ์ง์—์„œ ํ•„์š”ํ• ๋•Œ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
supabase/functions/push ํ•˜์œ„์— enqueuePushNotifications.ts ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
import { supabase } from "../api/lib/supabase.ts"; interface PushNotificationParams { userId: string; body?: string; title?: string; } // fcm_notifications ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ํŠน์ • ์œ ์ €์—๊ฒŒ ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ๋ฐœ์†กํ•˜๋„๋ก ์ ์žฌ export const enqueuePushNotification = async ( { userId, body, title }: PushNotificationParams, ) => { const { data, error } = await supabase .from("fcm_notifications") .insert([ { user_id: userId, body: body || null, title: title || null }, ]); if (error) { console.error("Error enqueuing push notification:", error); throw error; } return data; };
TypeScript
๋ณต์‚ฌ
์ด์ œ ๋ฐฑ์•ค๋“œ ๋กœ์ง์—์„œ ์•„๋ž˜ ์ฝ”๋“œ์ฒ˜๋Ÿผ ํ‘ธ์‹œ๋ฉ”์‹œ์ง€ ๋ฐœ์†ก์„ ์š”์ฒญํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
await enqueuePushNotification({ userId: userID, body: "ํ‘ธ์‹œ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐœ์†กํ•ฉ๋‹ˆ๋‹ค.", })
TypeScript
๋ณต์‚ฌ

๊ฐ•์˜ ์ฝ”๋“œ

9
pull