AI 예약 사이트에 이메일 알림 추가하기

Supabase, Resend, Edge Functions로 예약 확인 이메일과 사장님 알림을 보냅니다.

로드맵과 자료

처음이신가요? 여기서 시작하세요

이 가이드는 AI 예약 사이트 프로젝트에 자동 이메일 알림을 더합니다. 아직 예약 사이트를 만들지 않았다면 메인 가이드부터 먼저 진행하세요.

AI로 예약 사이트 만들기

메인 가이드 열기

MAIN GUIDE

https://profitstudio.app/video/build-booking-website-ai

예약 이메일용 Resend 설정하기

Resend 계정 생성 및 설정

Resend 열기

https://resend.com/

Resend 계정 생성 Resend API 키 생성 API 키를 복사해 안전하게 보관

중요

Resend API 키는 비공개로 유지하세요. 프런트엔드 코드에 추가하거나 공개적으로 공유하지 마세요.

발신 도메인 추가 및 인증

예약 웹사이트 도메인 추가 가장 가까운 리전 선택 Resend에서 DNS 레코드 복사 도메인 제공업체에 레코드 추가 Resend로 돌아가 도메인 인증 확인

DNS 인증은 도메인 제공업체에 따라 몇 분 이상 걸릴 수 있습니다.

예약 이메일 함수 만들기

예약 이메일 Edge Function 생성

Supabase 열기

https://supabase.com/?utm_source=partner&utm_medium=social&utm_campaign=supasquad&dub_id=faVfKdcLWvKCbYNp

Supabase 프로젝트 열기 Edge Functions 열기 브라우저 편집기로 새 함수 만들기 함수 이름을 `send-booking-email`로 지정 기본 스타터 코드 삭제

함수 코드 커스터마이즈 및 추가

소유자 이메일 입력 인증된 Resend 도메인 입력 발신 비즈니스 이름 입력 필요한 경우 이메일 템플릿 커스터마이즈 생성된 함수 코드 복사 후 붙여넣기

예약 이메일 함수

import { serve } from "https://deno.land/std/http/server.ts"; serve(async (req) => { try { const body = await req.json(); const record = body.record || {}; const resendKey = Deno.env.get("RESEND_API_KEY"); const supabaseUrl = Deno.env.get("SUPABASE_URL"); const supabaseServiceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"); if (!resendKey) { return new Response( JSON.stringify({ error: "Missing RESEND_API_KEY secret" }), { status: 500, headers: { "Content-Type": "application/json" }, } ); } const ownerEmail = "YOUR_OWNER_EMAIL"; const customerEmail = record.email || ""; const customerName = record.full_name || record.name || "Customer"; const phone = record.phone || ""; const appointmentDate = record.appointment_date || record.date || ""; const startTime = record.start_time || record.time || ""; const endTime = record.end_time || ""; const notes = record.notes || ""; const status = record.status || "New"; let serviceName = record.service_name || record.service || record.service_title || ""; // If the appointment only has service_id, fetch the service name from the services table if (!serviceName && record.service_id && supabaseUrl && supabaseServiceRoleKey) { try { const serviceId = encodeURIComponent(record.service_id); const serviceResponse = await fetch( `${supabaseUrl}/rest/v1/services?id=eq.${serviceId}&select=name`, { method: "GET", headers: { apikey: supabaseServiceRoleKey, Authorization: `Bearer ${supabaseServiceRoleKey}`, "Content-Type": "application/json", }, } ); const serviceData = await serviceResponse.json(); if (Array.isArray(serviceData) && serviceData.length > 0) { serviceName = serviceData[0].name || ""; } } catch (_serviceError) { serviceName = ""; } } if (!serviceName) { serviceName = "Appointment"; } // 1) Email to business owner const ownerResponse = await fetch("https://api.resend.com/emails", { method: "POST", headers: { Authorization: `Bearer ${resendKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ from: "Bookings <bookings@YOUR_DOMAIN_NAME>", to: [ownerEmail], subject: "OWNER_NOTIFICATION_SUBJECT", html: ` <h2>OWNER_NOTIFICATION_HEADING</h2> <p>OWNER_NOTIFICATION_INTRO</p> <p><b>Customer Name:</b> ${customerName}</p> <p><b>Email:</b> ${customerEmail}</p> <p><b>Phone:</b> ${phone}</p> <p><b>Service:</b> ${serviceName}</p> <p><b>Date:</b> ${appointmentDate}</p> <p><b>Start Time:</b> ${startTime}</p> <p><b>End Time:</b> ${endTime}</p> <p><b>Notes:</b> ${notes}</p> <p><b>Status:</b> ${status}</p> `, }), }); const ownerData = await ownerResponse.json(); // 2) Email to customer let customerData = null; if (customerEmail) { const customerResponse = await fetch("https://api.resend.com/emails", { method: "POST", headers: { Authorization: `Bearer ${resendKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ from: "YOUR_BUSINESS_NAME <bookings@YOUR_DOMAIN_NAME>", to: [customerEmail], subject: "CUSTOMER_CONFIRMATION_SUBJECT", html: ` <h2>CUSTOMER_CONFIRMATION_HEADING</h2> <p>Hi ${customerName},</p> <p>CUSTOMER_CONFIRMATION_INTRO</p> <p><b>Service:</b> ${serviceName}</p> <p><b>Date:</b> ${appointmentDate}</p> <p><b>Start Time:</b> ${startTime}</p> <p><b>End Time:</b> ${endTime}</p> <p>If you need to make any changes, please contact us before your appointment.</p> <p>CUSTOMER_CONFIRMATION_CLOSING</p> `, }), }); customerData = await customerResponse.json(); } return new Response( JSON.stringify({ success: true, ownerEmailSent: ownerData, customerEmailSent: customerData, }), { headers: { "Content-Type": "application/json" }, } ); } catch (error) { return new Response( JSON.stringify({ success: false, error: String(error), }), { status: 500, headers: { "Content-Type": "application/json" }, } ); } });

사장님 이메일

owner@example.com

인증된 Resend 도메인

yourdomain.com

발신 업체명

브라이트 치과

사장님 이메일 제목

사장님 이메일 제목 문구

사장님 이메일 첫 문구

고객 이메일 제목

고객 이메일 제목 문구

고객 이메일 첫 문구

고객 이메일 마무리 문구

함수 배포 및 설정

함수 배포 함수 설정 열기 `Verify JWT With Legacy Secret` 끄기 변경 사항 저장 웹훅 설정에 사용할 함수 엔드포인트 URL을 복사해 저장

중요

Database Webhook이 함수를 호출할 수 있도록 “Verify JWT With Legacy Secret”을 끄세요.

Resend API 키를 Supabase에 추가하기

Resend API 키를 Supabase 시크릿으로 추가

Edge Function Secrets 열기 새 시크릿 만들기 이름을 `RESEND_API_KEY`로 설정 Resend API 키 붙여넣기 시크릿 저장

중요

시크릿 이름을 정확히 `RESEND_API_KEY`로 사용하세요. 오타가 있으면 함수가 Resend에 접근할 수 없습니다.

신규 예약과 이메일 연결하기

예약 이메일 웹훅 생성

새 예약이 추가될 때마다 예약 이메일 함수를 실행하세요.

Integrations Database Webhooks

Database Webhooks 열기 Database Webhooks 활성화 새 웹훅 만들기 이름을 `send-booking-email-webhook`으로 지정 `public.appointments` 테이블 선택 이벤트로 `Insert` 선택 유형은 `HTTP Request`로 유지 요청 방식은 `POST` 사용 Edge Function 엔드포인트 URL 붙여넣기 웹훅 생성

예약 이메일 시스템 테스트하기

전체 예약 이메일 흐름 테스트

전체 예약 흐름을 테스트하고 두 이메일이 모두 정상적으로 전달되는지 확인하세요.

운영 중인 예약 웹사이트 열기 테스트 예약 제출 예약이 Supabase에 저장되었는지 확인 고객 확인 이메일 확인 소유자 알림 이메일 확인 두 이메일 모두 예약 정보가 정확한지 확인 예약이 관리자 대시보드에 표시되는지 확인