HANS17 AcademyAI · AGENT · LAB
Hans17Lecture NoteRAG LabFAQReference
강사 Agent
📄 M12 · 메일 발송 + 공유 페이지 — PDF인쇄 대화상자: 대상=PDF로 저장 · 용지=가로 · 색이 안 나오면 배경 그래픽 켜기 → 다크 16:9 슬라이드
HANS17 ACADEMY · AI Agent 실습 강의

M12 · 메일 발송 + 공유 페이지

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

hans17.com · 강의 노트 (16:9 슬라이드)

M12실습 · 45분

메일 발송 + 공유 페이지

토큰 기반 공유 + 후속 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;
}

html과 text를 동시에 넣으면 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에 shareUrl과 askUrl을 자동 삽입해 수신자가 추가 행동을 바로 취할 수 있게 한다.
  • 공유 토큰은 반드시 crypto.getRandomValues(16 bytes)로 생성해 128비트 예측 불가능성을 확보한다.
  • 보고서는 Vercel Blob에 reports/{token}.json으로 저장하고 expiresAt 필드를 포함한다.
  • /share/[token]은 로그인 없이 접근 가능하며, /share/[token]/ask는 원본 보고서를 컨텍스트로 단일 LLM 호출로 후속 Q&A를 처리한다.
  • 만료 토큰 접근 시 404 또는 안내 페이지로 명확히 처리해야 보안 사고를 예방할 수 있다.

🤖 Claude Code 프롬프트 (이렇게 시키면 됩니다)

이 모듈은 손코딩이 아니라 Claude Code에 프롬프트로 지시해서 만듭니다.

"Resend로 HTML+text 멀티파트 메일 발송을 만들고, footer에 share·ask URL을 자동 합성해줘." "crypto.getRandomValues(16바이트) 토큰으로 비로그인 공유 페이지(/share/[token])와 후속 Q&A 라우트를 만들어줘."

팁 · 파일을 첨부하려면 서버 발송(Resend), 본문·링크만이면 mailto.

🧪 실습(Lab)

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

Hans17ACADEMY

주중엔 엔지니어, 주말엔 아티스트 — 논리와 감성, 과학과 사운드의 경계를 넘나든다.

COURSE
  • 강사소개
  • Lecture Note
  • Reference
MUSIC
  • TikTok
  • YouTube
  • Apple Music
  • Instagram
  • Facebook
  • SoundCloud
© 2026 Hans17. Weekend Artist · Engineer · DJ.Why 17? — keep asking.
AgentAgent