Add contact form backend and deployment guide

- Resend API 통합 (이메일 발송)
- ContactForm 클라이언트 컴포넌트 생성
- API Route (/api/contact) 구현
- 입력 검증 및 에러 처리
- 성공/실패 메시지 표시
- 환경변수 설정 (.env.local, .env.example)
- 배포 가이드 작성 (DEPLOYMENT.md)
  - Resend 설정 방법
  - Vercel 배포 가이드
  - 가비아 도메인 연결 방법
  - 트러블슈팅 가이드

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 02:01:05 +09:00
parent 81e81aa810
commit e05def83d6
6 changed files with 862 additions and 73 deletions

237
DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,237 @@
# 🚀 배포 가이드
## 1⃣ Resend 설정 (이메일 발송)
### 1단계: Resend 계정 생성
1. [resend.com](https://resend.com) 접속
2. "Sign Up" 클릭 → GitHub 계정으로 가입
3. 무료 티어: 월 3,000건 (충분함!)
### 2단계: API Key 발급
1. Dashboard → "API Keys" 메뉴
2. "Create API Key" 클릭
3. Name: `jaengseung-made-production`
4. Permission: `Sending access`
5. 생성된 키 복사 (한 번만 표시됨!)
### 3단계: 도메인 인증 (선택사항)
**옵션 A: Resend 서브도메인 사용 (간단)**
- `onboarding@resend.dev`에서 발송
- 바로 사용 가능
**옵션 B: 커스텀 도메인 사용 (전문적)**
1. Resend Dashboard → "Domains"
2. "Add Domain" 클릭
3. `jaengseung-made.com` 입력
4. DNS 레코드 복사
**가비아 DNS 설정:**
1. [가비아 My가비아](https://my.gabia.com) 로그인
2. "도메인" → "jaengseung-made.com" 선택
3. "DNS 정보" → "DNS 관리" 클릭
4. Resend에서 제공한 레코드 추가:
```
Type: TXT
Host: _resend
Value: [Resend에서 제공한 값]
Type: MX
Host: @
Value: [Resend에서 제공한 값]
Priority: 10
```
5. 저장 후 10~30분 대기
6. Resend에서 "Verify" 클릭
### 4단계: 로컬 환경변수 설정
`.env.local` 파일 수정:
```bash
RESEND_API_KEY=re_your_actual_api_key_here
```
---
## 2⃣ Vercel 배포
### 1단계: GitHub 연동 (권장)
**GitHub에 푸시:**
```bash
# GitHub에 새 저장소 생성 (jaengseung-made)
git remote add github https://github.com/your-username/jaengseung-made.git
git push github main
```
**Vercel 배포:**
1. [vercel.com](https://vercel.com) 가입 (GitHub 연동)
2. "Add New Project" 클릭
3. GitHub 저장소 선택: `jaengseung-made`
4. Environment Variables 추가:
- `RESEND_API_KEY`: [발급받은 키 붙여넣기]
5. "Deploy" 클릭
6. 배포 완료! (약 2분)
### 2단계: 도메인 연결 (jaengseung-made.com)
**Vercel 설정:**
1. Vercel Dashboard → 프로젝트 선택
2. "Settings" → "Domains"
3. "Add Domain" 클릭
4. `jaengseung-made.com` 입력
5. DNS 설정 안내 확인
**가비아 DNS 설정:**
1. [가비아 My가비아](https://my.gabia.com) 로그인
2. "도메인" → "jaengseung-made.com" 선택
3. "DNS 정보" → "DNS 관리" 클릭
4. 기존 A 레코드 삭제 (있다면)
5. 새 레코드 추가:
**방법 A: A 레코드 (권장)**
```
Type: A
Host: @
Value: 76.76.19.19
TTL: 600
```
**방법 B: CNAME 레코드**
```
Type: CNAME
Host: @
Value: cname.vercel-dns.com.
TTL: 600
```
**www 서브도메인 추가:**
```
Type: CNAME
Host: www
Value: cname.vercel-dns.com.
TTL: 600
```
6. 저장 후 10~30분 대기
7. Vercel에서 자동으로 SSL 인증서 발급 (HTTPS)
### 3단계: 배포 확인
- https://jaengseung-made.com 접속
- 문의 폼 테스트 (실제 이메일 발송 확인)
---
## 3⃣ 대안: Gitea + Vercel CLI 직접 배포
Vercel CLI로 직접 배포:
```bash
# Vercel CLI 설치
npm install -g vercel
# 프로젝트 폴더에서 실행
cd C:\Users\박재오\Desktop\workspace\jaengseung-made
# 로그인
vercel login
# 배포
vercel --prod
# 환경변수 추가
vercel env add RESEND_API_KEY
# [발급받은 키 붙여넣기]
# Production, Preview, Development 모두 선택
# 재배포
vercel --prod
```
---
## 4⃣ Synology NAS 배포 (비추천)
성능/안정성 이유로 비추천하지만, 원하시면:
### Docker로 배포
1. `Dockerfile` 생성 (이미 있음)
2. Docker 이미지 빌드:
```bash
docker build -t jaengseung-made .
```
3. 컨테이너 실행:
```bash
docker run -d -p 3000:3000 \
-e RESEND_API_KEY=your_key \
--name jaengseung-made \
jaengseung-made
```
4. 포트 포워딩: 라우터에서 80 → NAS IP:3000
5. 가비아 DNS: A 레코드를 공인 IP로 설정
**문제점:**
- 느린 속도 (가정용 인터넷)
- 다운타임 (정전, 재부팅)
- HTTPS 수동 설정 필요 (Let's Encrypt)
- 보안 관리 필요
---
## 5⃣ 최종 체크리스트
### 배포 전
- [ ] Resend 계정 생성 및 API Key 발급
- [ ] `.env.local`에 API Key 설정
- [ ] 로컬에서 문의 폼 테스트 (http://localhost:3000)
- [ ] Git 커밋 및 푸시
### Vercel 배포
- [ ] Vercel 계정 생성 (GitHub 연동)
- [ ] 프로젝트 배포
- [ ] 환경변수 추가 (RESEND_API_KEY)
- [ ] 배포 URL에서 테스트
### 도메인 연결
- [ ] Vercel에 도메인 추가
- [ ] 가비아 DNS 설정 (A 레코드)
- [ ] www 서브도메인 추가 (CNAME)
- [ ] SSL 인증서 자동 발급 확인 (10~30분)
- [ ] https://jaengseung-made.com 접속 확인
### 최종 테스트
- [ ] 문의 폼 실제 발송 테스트
- [ ] bgg8988@gmail.com으로 이메일 수신 확인
- [ ] 모바일에서 접속 테스트
- [ ] 모든 링크 동작 확인
### 마케팅
- [ ] 크몽 서비스 등록 (포트폴리오 URL 첨부)
- [ ] 숨고 프로필 생성
- [ ] Google Search Console 등록
- [ ] 메타 태그 확인 (이미 적용됨)
---
## 🆘 트러블슈팅
### 문의 폼이 작동하지 않아요
1. `.env.local`에 `RESEND_API_KEY` 확인
2. Vercel 환경변수 설정 확인
3. Resend Dashboard에서 "Logs" 확인
4. 브라우저 개발자 도구 → Network 탭 확인
### 도메인이 연결되지 않아요
1. DNS 전파 대기 (최대 24시간, 보통 30분)
2. DNS 전파 확인: https://dnschecker.org
3. 가비아 DNS 설정 재확인
4. Vercel Domain 상태 확인
### 이메일이 오지 않아요
1. Resend Dashboard → "Logs" 확인
2. 스팸 메일함 확인
3. API Key 권한 확인 (Sending access)
4. 도메인 인증 상태 확인 (커스텀 도메인 사용 시)
---
## 📞 지원
배포 관련 문의: bgg8988@gmail.com

52
app/api/contact/route.ts Normal file
View File

@@ -0,0 +1,52 @@
import { NextResponse } from 'next/server';
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function POST(request: Request) {
try {
const body = await request.json();
const { name, phone, email, service, message } = body;
// 입력 검증
if (!name || !email || !message) {
return NextResponse.json(
{ error: '필수 항목을 모두 입력해주세요.' },
{ status: 400 }
);
}
// 이메일 발송
const data = await resend.emails.send({
from: 'contact@jaengseung-made.com', // Resend에서 인증된 도메인
to: ['bgg8988@gmail.com'], // 받는 이메일
subject: `[쟁승메이드] 새로운 문의: ${service || '문의'}`,
html: `
<h2>새로운 프로젝트 문의가 도착했습니다</h2>
<hr />
<p><strong>이름:</strong> ${name}</p>
<p><strong>연락처:</strong> ${phone || '미입력'}</p>
<p><strong>이메일:</strong> ${email}</p>
<p><strong>서비스:</strong> ${service || '미선택'}</p>
<hr />
<h3>문의 내용:</h3>
<p style="white-space: pre-wrap;">${message}</p>
<hr />
<p style="color: #666; font-size: 12px;">
이 메일은 jaengseung-made.com의 문의 폼에서 발송되었습니다.
</p>
`,
});
return NextResponse.json(
{ success: true, message: '문의가 성공적으로 전송되었습니다!' },
{ status: 200 }
);
} catch (error) {
console.error('Email send error:', error);
return NextResponse.json(
{ error: '메일 전송에 실패했습니다. 다시 시도해주세요.' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,186 @@
'use client';
import { useState } from 'react';
export default function ContactForm() {
const [formData, setFormData] = useState({
name: '',
phone: '',
email: '',
service: 'RPA 자동화',
message: '',
});
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [errorMessage, setErrorMessage] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus('loading');
setErrorMessage('');
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || '문의 전송에 실패했습니다.');
}
setStatus('success');
// 폼 초기화
setFormData({
name: '',
phone: '',
email: '',
service: 'RPA 자동화',
message: '',
});
// 3초 후 성공 메시지 숨기기
setTimeout(() => setStatus('idle'), 5000);
} catch (error) {
setStatus('error');
setErrorMessage(error instanceof Error ? error.message : '문의 전송에 실패했습니다.');
}
};
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => {
setFormData((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
};
return (
<div className="bg-white rounded-2xl shadow-xl p-8 md:p-12">
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">
<span className="text-red-500">*</span>
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
required
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
placeholder="홍길동"
disabled={status === 'loading'}
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2"></label>
<input
type="tel"
name="phone"
value={formData.phone}
onChange={handleChange}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
placeholder="010-0000-0000"
disabled={status === 'loading'}
/>
</div>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">
<span className="text-red-500">*</span>
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
required
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
placeholder="example@email.com"
disabled={status === 'loading'}
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2"> </label>
<select
name="service"
value={formData.service}
onChange={handleChange}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
disabled={status === 'loading'}
>
<option>RPA </option>
<option> </option>
<option> </option>
<option> </option>
</select>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">
<span className="text-red-500">*</span>
</label>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
required
rows={6}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none resize-none"
placeholder="프로젝트에 대해 자세히 설명해주세요. 목적, 예상 기간, 예산 등을 포함하면 더 정확한 상담이 가능합니다."
disabled={status === 'loading'}
></textarea>
</div>
{status === 'success' && (
<div className="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded-lg">
! 24 .
</div>
)}
{status === 'error' && (
<div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg">
{errorMessage}
</div>
)}
<button
type="submit"
disabled={status === 'loading'}
className="w-full bg-blue-700 text-white py-4 rounded-lg text-lg font-bold hover:bg-blue-800 transition shadow-lg disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{status === 'loading' ? '전송 중...' : '무료 상담 신청하기'}
</button>
</form>
<div className="mt-8 pt-8 border-t border-gray-200">
<div className="text-center text-gray-600">
<p className="mb-4"> </p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a
href="mailto:bgg8988@gmail.com"
className="flex items-center justify-center text-blue-700 hover:text-blue-800"
>
<span className="mr-2">📧</span> bgg8988@gmail.com
</a>
<a
href="tel:010-3907-1392"
className="flex items-center justify-center text-blue-700 hover:text-blue-800"
>
<span className="mr-2">📱</span> 010-3907-1392
</a>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,3 +1,5 @@
import ContactForm from './components/ContactForm';
export default function Home() {
return (
<div className="min-h-screen">
@@ -331,77 +333,7 @@ export default function Home() {
<p className="text-xl text-gray-600"> . 24 .</p>
</div>
<div className="bg-white rounded-2xl shadow-xl p-8 md:p-12">
<form className="space-y-6">
<div className="grid md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2"></label>
<input
type="text"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
placeholder="홍길동"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2"></label>
<input
type="tel"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
placeholder="010-0000-0000"
/>
</div>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2"></label>
<input
type="email"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
placeholder="example@email.com"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2"> </label>
<select className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none">
<option>RPA </option>
<option> </option>
<option> </option>
<option> </option>
</select>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2"> </label>
<textarea
rows={6}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none resize-none"
placeholder="프로젝트에 대해 자세히 설명해주세요. 목적, 예상 기간, 예산 등을 포함하면 더 정확한 상담이 가능합니다."
></textarea>
</div>
<button
type="submit"
className="w-full bg-blue-700 text-white py-4 rounded-lg text-lg font-bold hover:bg-blue-800 transition shadow-lg"
>
</button>
</form>
<div className="mt-8 pt-8 border-t border-gray-200">
<div className="text-center text-gray-600">
<p className="mb-4"> </p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a href="mailto:bgg8988@gmail.com" className="flex items-center justify-center text-blue-700 hover:text-blue-800">
<span className="mr-2">📧</span> bgg8988@gmail.com
</a>
<a href="tel:010-3907-1392" className="flex items-center justify-center text-blue-700 hover:text-blue-800">
<span className="mr-2">📱</span> 010-3907-1392
</a>
</div>
</div>
</div>
</div>
<ContactForm />
</div>
</section>

383
package-lock.json generated
View File

@@ -10,7 +10,8 @@
"dependencies": {
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3"
"react-dom": "19.2.3",
"resend": "^6.9.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@@ -1233,6 +1234,25 @@
"dev": true,
"license": "MIT"
},
"node_modules/@selderee/plugin-htmlparser2": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
"integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==",
"license": "MIT",
"dependencies": {
"domhandler": "^5.0.3",
"selderee": "^0.11.0"
},
"funding": {
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/@stablelib/base64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
"license": "MIT"
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -2113,6 +2133,17 @@
"win32"
]
},
"node_modules/@zone-eu/mailsplit": {
"version": "5.4.8",
"resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.8.tgz",
"integrity": "sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA==",
"license": "(MIT OR EUPL-1.1+)",
"dependencies": {
"libbase64": "1.3.0",
"libmime": "5.3.7",
"libqp": "2.1.1"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -2718,6 +2749,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -2777,6 +2817,61 @@
"node": ">=0.10.0"
}
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2806,6 +2901,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/encoding-japanese": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.2.0.tgz",
"integrity": "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==",
"license": "MIT",
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/enhanced-resolve": {
"version": "5.19.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
@@ -2820,6 +2924,18 @@
"node": ">=10.13.0"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es-abstract": {
"version": "1.24.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz",
@@ -3495,6 +3611,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-sha256": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
"license": "Unlicense"
},
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
@@ -3873,6 +3995,15 @@
"node": ">= 0.4"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"license": "MIT",
"bin": {
"he": "bin/he"
}
},
"node_modules/hermes-estree": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
@@ -3890,6 +4021,57 @@
"hermes-estree": "0.25.1"
}
},
"node_modules/html-to-text": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
"integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==",
"license": "MIT",
"dependencies": {
"@selderee/plugin-htmlparser2": "^0.11.0",
"deepmerge": "^4.3.1",
"dom-serializer": "^2.0.0",
"htmlparser2": "^8.0.2",
"selderee": "^0.11.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/htmlparser2": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1",
"entities": "^4.4.0"
}
},
"node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4512,6 +4694,15 @@
"node": ">=0.10"
}
},
"node_modules/leac": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
"integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==",
"license": "MIT",
"funding": {
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -4526,6 +4717,42 @@
"node": ">= 0.8.0"
}
},
"node_modules/libbase64": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.3.0.tgz",
"integrity": "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==",
"license": "MIT"
},
"node_modules/libmime": {
"version": "5.3.7",
"resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz",
"integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==",
"license": "MIT",
"dependencies": {
"encoding-japanese": "2.2.0",
"iconv-lite": "0.6.3",
"libbase64": "1.3.0",
"libqp": "2.1.1"
}
},
"node_modules/libmime/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/libqp": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.1.tgz",
"integrity": "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==",
"license": "MIT"
},
"node_modules/lightningcss": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
@@ -4787,6 +5014,15 @@
"url": "https://opencollective.com/parcel"
}
},
"node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"license": "MIT",
"dependencies": {
"uc.micro": "^2.0.0"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -4843,6 +5079,24 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/mailparser": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.9.1.tgz",
"integrity": "sha512-6vHZcco3fWsDMkf4Vz9iAfxvwrKNGbHx0dV1RKVphQ/zaNY34Buc7D37LSa09jeSeybWzYcTPjhiZFxzVRJedA==",
"license": "MIT",
"dependencies": {
"@zone-eu/mailsplit": "5.4.8",
"encoding-japanese": "2.2.0",
"he": "1.2.0",
"html-to-text": "9.0.5",
"iconv-lite": "0.7.0",
"libmime": "5.3.7",
"linkify-it": "5.0.0",
"nodemailer": "7.0.11",
"punycode.js": "2.3.1",
"tlds": "1.261.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -5036,6 +5290,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/nodemailer": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.11.tgz",
"integrity": "sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -5240,6 +5503,19 @@
"node": ">=6"
}
},
"node_modules/parseley": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
"integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==",
"license": "MIT",
"dependencies": {
"leac": "^0.6.0",
"peberminta": "^0.9.0"
},
"funding": {
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -5267,6 +5543,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/peberminta": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
"integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==",
"license": "MIT",
"funding": {
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -5357,6 +5642,15 @@
"node": ">=6"
}
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -5450,6 +5744,27 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resend": {
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/resend/-/resend-6.9.1.tgz",
"integrity": "sha512-jFY3qPP2cith1npRXvS7PVdnhbR1CcuzHg65ty5Elv55GKiXhe+nItXuzzoOlKeYJez1iJAo2+8f6ae8sCj0iA==",
"license": "MIT",
"dependencies": {
"mailparser": "3.9.1",
"svix": "1.84.1"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@react-email/render": "*"
},
"peerDependenciesMeta": {
"@react-email/render": {
"optional": true
}
}
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -5581,12 +5896,30 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
"node_modules/selderee": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
"integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==",
"license": "MIT",
"dependencies": {
"parseley": "^0.12.0"
},
"funding": {
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -5819,6 +6152,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/standardwebhooks": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
"license": "MIT",
"dependencies": {
"@stablelib/base64": "^1.0.0",
"fast-sha256": "^1.3.0"
}
},
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@@ -6018,6 +6361,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svix": {
"version": "1.84.1",
"resolved": "https://registry.npmjs.org/svix/-/svix-1.84.1.tgz",
"integrity": "sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==",
"license": "MIT",
"dependencies": {
"standardwebhooks": "1.0.0",
"uuid": "^10.0.0"
}
},
"node_modules/tailwindcss": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
@@ -6087,6 +6440,15 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/tlds": {
"version": "1.261.0",
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz",
"integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==",
"license": "MIT",
"bin": {
"tlds": "bin.js"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -6274,6 +6636,12 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"license": "MIT"
},
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
@@ -6376,6 +6744,19 @@
"punycode": "^2.1.0"
}
},
"node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -11,7 +11,8 @@
"dependencies": {
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3"
"react-dom": "19.2.3",
"resend": "^6.9.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",