Computer >> 컴퓨터 >  >> 프로그래밍 >> Redis

원할 때 정확히 이메일 알림 전달:QStash를 통한 시간대 인식 일정

docsly에서는 지난 주 또는 달에 받은 모든 피드백의 요약과 함께 사용자에게 이메일 알림을 보내는 새로운 기능을 출시했습니다. 이메일을 보내는 것이 새로운 문제는 아니지만, 이와 관련하여 최고의 사용자 경험을 제공하고 싶었기 때문에 이상한 시간에 이메일을 보내는 것을 피하기 위해 모든 이메일은 사용자의 시간대에 전송되어야 한다고 결정했습니다. 또한 우리는 사용자에게 이메일 수신 빈도를 선택할 수 있는 기능을 제공하고 싶었습니다. 또한 우리는 사용자가 언제든지 예약된 이메일 알림을 취소할 수 있는 기능을 제공하고 싶었습니다.

이 솔루션을 구현하는 것은 다음과 같은 이유로 까다로웠습니다.

  1. 우리는 사용자의 시간대를 데이터베이스에 저장하고 싶지 않았습니다.
  2. 또한 이메일을 보낼 시간인지 확인하기 위해 매분 또는 시간마다 cron 작업을 실행하고 싶지 않았습니다.
  3. 또한 사용자가 예약된 이메일 알림을 취소할 수 있도록 하고 싶었습니다.

그래서 우리는 고유한 솔루션을 생각해냈습니다. 사용자의 시간대에 크론 작업을 예약한 다음 사용자가 이메일 알림을 취소하면 크론 작업을 취소하는 것입니다. 하지만 한 가지 질문이 남았습니다. 어떻게?

우리는 이미 Upstash를 Redis 스토어로 사용하고 있었고 QStash가 일정 기능도 지원한다는 것을 알았습니다. 좀 더 자세히 살펴보면 QStash가 CRON 표현식도 지원한다는 사실을 발견했습니다. 그래서 우리는 QStash를 사용하여 크론 작업을 예약하기로 결정했습니다.

이 기사에서는 Next.js 애플리케이션에서 QStash 및 Upstash Redis를 사용하여 사용자의 시간대에 이메일을 예약하는 프로세스를 안내합니다. GitHub에서 전체 소스 코드를 찾을 수도 있습니다.

사용자의 시간대로 이메일을 예약하기 위한 Next.js 애플리케이션 구축

전제조건

이 튜토리얼을 따라가려면 다음이 필요합니다:

  • Upstash 계정
  • Node.js 개발 환경

프로젝트 설정

시작하려면 다음 명령을 사용하여 새 Next.js 프로젝트를 만드세요:

npx create-next-app qstash-email-scheduling

다음으로, Upstash와 상호작용하려면 다음 종속성을 설치하세요:

npm install --save @upstash/redis axios

.env.local 만들기 프로젝트 루트에 파일을 저장하고 Upstash 계정에서 다음 환경 변수를 추가하세요:

UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
QSTASH_URL=
QSTASH_TOKEN=
QSATSH_CURRENT_SIGNING_KEY=
QSATSH_NEXT_SIGNING_KEY=

솔루션 개요

코드를 구현하기 전에 솔루션 개요를 살펴보겠습니다. 세 개의 Next.js API 경로를 생성하겠습니다:

  • POST /api/schedule-cron - 사용자의 시간대로 이메일 크론 작업을 예약합니다.
  • POST /api/cancel-schedule - 예약된 이메일 크론 작업을 취소합니다.
  • POST /api/send-email - 이메일을 보내기 위해 예약된 크론 작업에 의해 실행됩니다.

API 경로 외에도 사용자가 선호하는 이메일 수신 시간을 수집하는 간단한 양식도 만들 예정입니다.

사용자 인터페이스 만들기

사용자 인터페이스를 만들기 위해 app/page.tsx에 새 페이지를 만듭니다. 다음 코드를 사용하세요:

"use client";
 
import { useState } from "react";
 
import axios from "axios";
 
export default function Home() {
 const userId = "tony-stark-11";
 const [selectedTime, setSelectedTime] = useState("10:00");
 
 async function createEmailNotificationSchedule() {
 try {
 await axios.post(
 "/api/schedule-cron",
 {
 userId,
 selectedTime,
 utcOffset: new Date().getTimezoneOffset(),
 },
 {
 headers: {
 "Content-Type": "application/json",
 },
 },
 );
 alert("Email notification scheduled");
 } catch (e) {
 console.log("Client side error", e);
 alert("Error scheduling email notification");
 }
 }
 
 async function cancelEmailNotificationSchedule() {
 try {
 await axios.post(
 "/api/cancel-schedule",
 {
 userId,
 },
 {
 headers: {
 "Content-Type": "application/json",
 },
 },
 );
 alert("Email notification schedule cancelled");
 } catch (e) {
 console.log("Client side error", e);
 alert("Error scheduling email notification");
 }
 }
 
 return (
 <main className="mx-auto flex min-h-screen max-w-md flex-col justify-center p-24">
 <h1 className="mb-4 text-xl font-bold text-neutral-600">
 Email Notification for {userId}
 </h1>
 Send daily email summary at:
 <select
 onChange={(e) => setSelectedTime(e.target.value)}
 className="mt-4 h-12 w-64 rounded-lg border-2 border-neutral-600 bg-neutral-800 p-2
 text-white"
 >
 {new Array(24).fill(0).map((_, i) => {
 const time = i < 10 ? `0${i}:00` : `${i}:00`;
 return (
 <option key={i} value={time}>
 {time}
 </option>
 );
 })}
 </select>
 <button
 className="mt-4 rounded bg-green-700 px-4 py-2 text-white"
 onClick={createEmailNotificationSchedule}
 >
 Schedule
 </button>
 <button
 className="mt-4 rounded bg-red-500 px-4 py-2 text-white"
 onClick={cancelEmailNotificationSchedule}
 >
 Cancel Schedule
 </button>
 </main>
 );
}

위의 코드는 사용자가 일일 이메일 요약을 받기 위해 선호하는 시간을 설정할 수 있는 드롭다운이 있는 양식을 만듭니다. 이러한 알림을 예약하거나 취소할 수 있으며 애플리케이션은 HTTP POST 요청을 사용하여 서버와 통신합니다. 다음 섹션에서 HTTP 엔드포인트를 생성하겠습니다.

원할 때 정확히 이메일 알림 전달:QStash를 통한 시간대 인식 일정

이메일 크론 작업 예약

원할 때 정확히 이메일 알림 전달:QStash를 통한 시간대 인식 일정

POST /api/schedule-cron을 만드는 것부터 시작하겠습니다. 경로. 이 경로는 사용자의 시간대로 이메일 크론 작업을 예약하는 데 사용됩니다. 크론 작업을 예약하기 위해 QStash 라이브러리를 사용할 것입니다.

import { NextApiRequest, NextApiResponse } from "next";
 
import { Redis } from "@upstash/redis";
import axios from "axios";
 
export const QSTASH_CONFIG = {
 QSTASH_URL: process.env.QSTASH_URL,
 QSTASH_TOKEN: process.env.QSTASH_TOKEN,
 QSTASH_CURRENT_SIGNING_KEY: process.env.QSTASH_CURRENT_SIGNING_KEY,
};
 
export const upstash = new Redis({
 url: process.env.UPSTASH_REDIS_REST_URL!,
 token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
 
// Edit this endpoint to match your domain
const SUMMARY_ENDPOINT = "https://<your-domain>/api/send-email";
 
export default async function scheduleSummary(
 req: NextApiRequest,
 res: NextApiResponse,
) {
 console.log("========SCHEDULE SUMMARY========");
 if (req.method !== "POST") {
 return res.status(400).json({ message: "bad request" });
 }
 const { body } = req;
 
 const { userId, selectedTime, utcOffset } = body;
 
 const emailScheduleKey = `email-schedule-${userId}`;
 
 const scheduleId = await upstash.get(emailScheduleKey);
 
 // remove existing schedule before creating a new one
 if (scheduleId) {
 try {
 await axios.delete(
 `https://qstash.upstash.io/v1/schedules/${scheduleId}`,
 {
 headers: {
 Authorization: `Bearer ${QSTASH_CONFIG.QSTASH_TOKEN}`,
 },
 },
 );
 } catch (e) {
 console.log("Schedule not found in QStash ");
 }
 await upstash.del(emailScheduleKey);
 }
 
 const [hour, min] = convertToUTC(selectedTime, utcOffset).split(":");
 const selectedCron = `${min} ${hour} * * *`;
 
 // create and store new schedule
 try {
 const { data, status } = await axios.post(
 `${QSTASH_CONFIG.QSTASH_URL}${SUMMARY_ENDPOINT}`,
 { userId },
 {
 headers: {
 "Content-Type": "application/json",
 Authorization: `Bearer ${QSTASH_CONFIG.QSTASH_TOKEN}`,
 "Upstash-Cron": selectedCron,
 },
 },
 );
 console.log({ data, status });
 if (data.scheduleId) {
 await upstash.set(emailScheduleKey, data.scheduleId);
 }
 } catch (e) {
 console.log({ e });
 }
 
 return res.status(200).json({ message: "success" });
}
 
function convertToUTC(timeString: string, utcOffset: number) {
 const [hours, minutes] = timeString.split(":").map(Number);
 const timeInMinutes = hours * 60 + minutes;
 const utcTimeInMinutes = (timeInMinutes + utcOffset + 1440) % 1440;
 const utcHours = Math.floor(utcTimeInMinutes / 60);
 const utcMinutes = utcTimeInMinutes % 60;
 return `${utcHours.toString().padStart(2, "0")}:${utcMinutes
 .toString()
 .padStart(2, "0")}`;
}

코드의 핵심은 scheduleSummary API 엔드포인트인 함수입니다. 들어오는 POST 요청을 처리하고 다음 단계를 수행합니다.

  1. 요청 메소드를 검증하여 POST 요청인지 확인합니다.
  2. 요청 본문에서 userId, selectedTime 및 utcOffset을 포함한 데이터를 추출합니다.
  3. Upstash 데이터베이스에 사용자의 이메일 일정에 대한 키를 구성합니다.
  4. 사용자의 기존 일정을 검색하고 QStash에서 제거합니다.
  5. convertToUTC를 사용하여 사용자가 선택한 시간을 UTC 형식으로 변환합니다. 시간 문자열과 UTC 오프셋을 사용하여 오프셋을 고려하면서 시간을 UTC 형식으로 변환하는 함수입니다.
  6. QStash의 예약 기능을 사용하여 이메일 요약을 보내기 위한 새로운 일정을 만들고 userId를 추가합니다. /api/send-email의 페이로드로 엔드포인트가 수신됩니다.
  7. 새로 생성된 일정 ID를 Upstash 데이터베이스에 저장합니다.

이메일 요약 보내기

POST /api/send-email 이메일을 보내기 위해 예약된 크론 작업에 의해 경로가 트리거됩니다. @upstash/qstash/nextjs을 사용하겠습니다. 요청의 서명을 확인하는 라이브러리입니다. 이렇게 하면 요청이 다른 소스가 아닌 QStash에서 오는지 확인할 수 있습니다.

POST /api/send-email 함수는 userId를 수신합니다. 요청 본문에. userId를 기반으로 이메일을 준비하고 보내는 기능을 구현할 수 있습니다. .

import { NextApiRequest, NextApiResponse } from "next";
 
import { verifySignature } from "@upstash/qstash/nextjs";
 
async function handler(request: NextApiRequest, res: NextApiResponse) {
 console.log("==========Project summary handler==========");
 if (request.method !== "POST") {
 return res.status(400).json({ message: "bad request" });
 }
 const { body } = request;
 const { userId } = body;
 
 // prepare and send email
 
 return res.status(200).json({ message: "success" });
}
 
export default verifySignature(handler);
 
export const config = {
 api: {
 bodyParser: false,
 },
};

예약된 이메일 크론 작업 취소

원할 때 정확히 이메일 알림 전달:QStash를 통한 시간대 인식 일정

다음으로 POST /api/cancel-schedule을 만들어 보겠습니다. 경로. 이 경로는 예약된 이메일 크론 작업을 취소하는 데 사용됩니다. 크론 작업을 취소하기 위해 QStash 라이브러리를 사용할 것입니다.

import { NextApiRequest, NextApiResponse } from "next";
import axios from "axios";
import { Redis } from "@upstash/redis";
 
export const QSTASH_CONFIG = {
 QSTASH_URL: process.env.QSTASH_URL,
 QSTASH_TOKEN: process.env.QSTASH_TOKEN,
 QSTASH_CURRENT_SIGNING_KEY: process.env.QSTASH_CURRENT_SIGNING_KEY,
};
 
export const upstash = new Redis({
 url: process.env.UPSTASH_REDIS_REST_URL!,
 token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
 
export default async function scheduleSummary(
 req: NextApiRequest,
 res: NextApiResponse
) {
 console.log("========REMOVE SCHEDULE SUMMARY========");
 if (req.method !== "POST") {
 return res.status(400).json({ message: "bad request" });
 }
 const { body } = req;
 
 const { userId } = body;
 
 const emailScheduleKey = `email-schedule-${userId}`;
 
 const scheduleId = await upstash.get(emailScheduleKey);
 
 // remove existing schedule before creating a new one
 if (scheduleId) {
 try {
 await axios.delete(
 `https://qstash.upstash.io/v1/schedules/${scheduleId}`,
 {
 headers: {
 Authorization: `Bearer ${QSTASH_CONFIG.QSTASH_TOKEN}`,
 },
 }
 );
 } catch (e) {
 console.log("Schedule not found in QStash ");
 }
 await upstash.del(emailScheduleKey);
 }
 
 
 return res.status(200).json({ message: "success" });
}

결론

요약하자면, Upstash Redis와 QStash를 사용하여 사용자의 시간대에 맞춰 이메일을 예약할 수 있었습니다. 우리는 사용자의 시간대를 데이터베이스에 저장하지 않고 이를 달성했습니다. 또한 사용자가 언제든지 예약된 이메일 알림을 취소할 수 있는 기능을 제공할 수 있었습니다.

제품에 대한 문서를 유지 관리하는 경우 docsly를 확인해야 합니다. 이는 사용자로부터 피드백을 수집하고 이를 실행 가능한 통찰력으로 전환하는 데 도움이 되는 기술 문서용으로 제작된 피드백 도구입니다.