← Lecture Note
M12실습45분 · ~12 슬라이드

메일 발송 + 공유 페이지

토큰 기반 공유 + 후속 Q&A

학습 목표

보고서를 메일로 보내고, 비로그인 공유 링크에서 후속 질의가 가능하게 한다.

보고서가 화면에 뜨는 것과, 받은 사람이 그 보고서를 바탕으로 후속 질문까지 할 수 있는 것은 UX 품질의 차이가 크다. 이 모듈에서는 Resend로 HTML 메일을 발송하고, 128비트 공유 토큰 기반의 비로그인 공유 페이지에서 후속 Q&A가 가능한 흐름을 완성한다.


Resend SDK로 메일 발송하기

이 강의는 메일 발송에 Resend를 사용한다. nodemailer는 SMTP 자격증명 관리가 번거롭고 Vercel Edge에서 동작이 불안정하기 때문이다.

항목nodemailerResend SDK
인증 방식SMTP 자격증명API Key (env var 1개)
Vercel 호환불안정 (Node.js 전용)Edge/Node 모두 지원
HTML 조립수동html + text 필드로 분리
발송 확인직접 구현응답 객체에 id 포함

설치 후 가장 기본적인 발송 코드는 아래와 같다.

// lib/mail.ts
import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY!);

export async function sendReportMail({
  to,
  subject,
  htmlBody,
  textBody,
}: {
  to: string;
  subject: string;
  htmlBody: string;
  textBody: string;
}) {
  const { data, error } = await resend.emails.send({
    from: "Hans17 Academy <noreply@aicrmeet.com>",
    to,
    subject,
    html: htmlBody,
    text: textBody, // 멀티파트: HTML 미지원 클라이언트용
  });
  if (error) throw new Error(`Resend error: ${error.message}`);
  return data?.id;
}

htmltext를 동시에 넣으면 Resend가 자동으로 multipart/alternative로 패키징한다. 스팸 필터 통과율을 높이기 위해 text 필드는 절대 생략하지 않는다.


HTML 본문 + 자동 footer 조립

보고서 본문은 이미 Markdown으로 생성되어 있다. 이것을 HTML로 변환한 뒤, 공유 URL후속 질의 URL이 담긴 footer를 덧붙인다.

// lib/buildReportHtml.ts
import { marked } from "marked";

export function buildReportHtml({
  markdownBody,
  shareToken,
  baseUrl,
}: {
  markdownBody: string;
  shareToken: string;
  baseUrl: string;
}) {
  const htmlBody = marked.parse(markdownBody);   // Markdown → HTML
  const shareUrl = `${baseUrl}/share/${shareToken}`;
  const askUrl   = `${baseUrl}/share/${shareToken}/ask`;

  const footer = `
    <hr style="margin:32px 0;border:none;border-top:1px solid #e5e7eb"/>
    <p style="font-size:13px;color:#6b7280">
      이 보고서를 공유하려면:
      <a href="${shareUrl}">${shareUrl}</a><br/>
      후속 질문하기:
      <a href="${askUrl}">${askUrl}</a>
    </p>`;

  return `<!DOCTYPE html><html><body>
    <div style="max-width:720px;margin:auto;font-family:sans-serif">
      ${htmlBody}
      ${footer}
    </div>
  </body></html>`;
}

text 버전도 같은 URL을 plain text로 추가하면 된다.


공유 토큰 생성 (128비트)

공유 링크는 예측 불가능한 토큰이 핵심이다. Math.random()은 절대 사용하지 않는다. Web Crypto API의 crypto.getRandomValues로 16바이트(128비트) 난수를 생성한다.

// lib/shareToken.ts
export function generateShareToken(): string {
  const bytes = new Uint8Array(16);
  crypto.getRandomValues(bytes);                      // Web Crypto — Node/Edge 모두 지원
  return Array.from(bytes)
    .map((b) => b.toString(16).padStart(2, "0"))
    .join(""); // 32자 hex 문자열
}

생성된 토큰은 Vercel Blob에 보고서 메타데이터와 함께 저장한다. 키 형식은 reports/{token}.json을 권장한다.

보안 주의: 토큰을 URL에 노출하더라도 128비트 무작위성 덕분에 브루트포스는 현실적으로 불가능하다. 단, 토큰을 DB에 저장할 때 만료 시각(expiresAt)을 반드시 포함하고, 공유 페이지 서버 코드에서 만료 여부를 먼저 확인한다.


비로그인 공유 페이지 + 후속 Q&A

/share/[token]/page.tsx는 로그인 미들웨어를 거치지 않는다. token으로 Blob에서 보고서를 읽어 렌더링만 하면 된다.

/share/[token]/ask/route.ts는 POST를 받아 원본 보고서를 컨텍스트로 삼아 Claude에 질의한다. 이때 Agent dispatcher를 따로 태우지 않고, 단일 LLM 호출로 간단하게 처리한다. 원본 보고서가 이미 충분한 컨텍스트이기 때문이다.


실습 (Lab)

목표: 보고서를 메일로 발송하고, 수신자가 공유 링크에서 후속 질의까지 완료한다.

Step 1 — 환경 변수 세팅

RESEND_API_KEY=re_xxxx
NEXT_PUBLIC_BASE_URL=https://your-domain.vercel.app

Step 2 — 토큰 생성 및 Blob 저장

generateShareToken()으로 토큰을 만들고, reports/{token}.json{ reportMarkdown, createdAt, expiresAt } 형태로 put()한다.

Step 3 — 메일 발송 API Route 작성

app/api/report/send/route.ts를 POST로 작성. 요청 바디에서 to, reportId를 받아 Blob에서 Markdown을 꺼낸 뒤 buildReportHtml()sendReportMail()을 순서대로 호출한다.

Step 4 — 공유 페이지 라우트 추가

app/share/[token]/page.tsx: Blob에서 보고서 로드 → 만료 검사 → Markdown 렌더링. app/share/[token]/ask/route.ts: POST body의 question과 보고서 Markdown을 system prompt에 넣어 Claude 호출. streaming 응답 권장.

Step 5 — 수동 E2E 테스트

  1. 로컬에서 Send API를 curl로 호출해 메일 수신 확인.
  2. 메일 내 공유 링크를 시크릿 모드 브라우저(비로그인 상태)에서 열어 보고서가 뜨는지 확인.
  3. Ask 폼에 질문 입력 → 스트리밍 응답이 정상 출력되는지 확인.
  4. expiresAt을 과거 시각으로 바꿔 Blob을 수동 수정한 뒤, 만료 처리가 올바르게 동작하는지 검증.

: Resend 무료 플랜은 하루 100통 제한이 있다. 개발 중에는 실제 메일 발송 대신 console.log(htmlBody)로 확인하고, 최종 E2E 때만 실발송하면 발송 한도를 아낄 수 있다.


핵심 정리

  • Resend SDK는 html + text 멀티파트를 한 번의 호출로 처리하며, Vercel Edge에서도 안정적으로 동작한다.
  • 메일 footer에 shareUrlaskUrl을 자동 삽입해 수신자가 추가 행동을 바로 취할 수 있게 한다.
  • 공유 토큰은 반드시 crypto.getRandomValues(16 bytes)로 생성해 128비트 예측 불가능성을 확보한다.
  • 보고서는 Vercel Blob에 reports/{token}.json으로 저장하고 expiresAt 필드를 포함한다.
  • /share/[token]은 로그인 없이 접근 가능하며, /share/[token]/ask는 원본 보고서를 컨텍스트로 단일 LLM 호출로 후속 Q&A를 처리한다.
  • 만료 토큰 접근 시 404 또는 안내 페이지로 명확히 처리해야 보안 사고를 예방할 수 있다.
LAB · 실습

보고서 메일 자동 발송 + 받는 사람이 후속 질의