diff --git a/app/admin/quotes/[id]/page.tsx b/app/admin/quotes/[id]/page.tsx
index ef8db76..415095e 100644
--- a/app/admin/quotes/[id]/page.tsx
+++ b/app/admin/quotes/[id]/page.tsx
@@ -127,14 +127,21 @@ export default function QuoteEditorPage() {
}
// ── 고객에게 발송 ───────────────────────
+ const SENT_STATUSES = ['sent', 'accepted', 'rejected'];
+ const isSentStatus = SENT_STATUSES.includes(form.status);
+
async function sendToClient() {
- if (!form.client_email) return;
+ if (!form.client_email || isSentStatus) return;
if (!confirm("고객에게 견적 메일을 발송하고 상태를 '발송됨'으로 변경합니다.")) return;
setSending(true);
try {
const res = await fetch(`/api/admin/quotes/${id}/send`, { method: 'POST' });
const d = await res.json();
if (res.ok && d.success) {
+ if (d.alreadySent) {
+ alert('이미 발송된 견적입니다');
+ return;
+ }
setField('status', 'sent');
if (d.emailSent === false) {
alert('상태는 변경됐으나 메일 발송에 실패했습니다 — 수동 발송이 필요합니다');
@@ -285,17 +292,21 @@ export default function QuoteEditorPage() {
{/* 고객에게 발송 */}
- {!form.client_email && (
+ {!form.client_email && !isSentStatus && (
이메일 입력 필요
)}
diff --git a/app/api/admin/quotes/[id]/send/route.ts b/app/api/admin/quotes/[id]/send/route.ts
index 54a338f..d806e35 100644
--- a/app/api/admin/quotes/[id]/send/route.ts
+++ b/app/api/admin/quotes/[id]/send/route.ts
@@ -31,16 +31,21 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
return NextResponse.json({ error: '견적서를 찾을 수 없습니다' }, { status: 404 });
}
- // 2. 고객 이메일 필수
+ // 2. 이미 발송/수락/거절된 견적은 재발송 차단
+ if (['sent', 'accepted', 'rejected'].includes(quote.status)) {
+ return NextResponse.json({ success: true, emailSent: false, alreadySent: true });
+ }
+
+ // 3. 고객 이메일 필수
if (!quote.client_email) {
return NextResponse.json({ error: '고객 이메일을 먼저 입력하세요' }, { status: 400 });
}
- // 3. public_token 보장
+ // 4. public_token 보장
const quoteToken: string = quote.public_token || crypto.randomUUID();
const nowIso = new Date().toISOString();
- // 4. 견적 상태 업데이트
+ // 5. 견적 상태 업데이트
const updatePayload: Record = { status: 'sent', updated_at: nowIso };
if (!quote.public_token) updatePayload.public_token = quoteToken;
@@ -54,7 +59,7 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
return NextResponse.json({ error: '견적 상태 업데이트 실패' }, { status: 500 });
}
- // 5. 연결된 의뢰 상태 동기화 (실패해도 진행)
+ // 6. 연결된 의뢰 상태 동기화 (실패해도 진행)
if (quote.contact_request_id) {
const { error: syncError } = await supabase
.from('contact_requests')
@@ -65,7 +70,7 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
}
}
- // 6. 견적 메일 발송 (실패해도 상태 변경은 유지)
+ // 7. 견적 메일 발송 (실패해도 상태 변경은 유지)
let emailSent = true;
try {
await sendQuoteSentEmail({
diff --git a/app/api/admin/quotes/route.ts b/app/api/admin/quotes/route.ts
index 822d65c..5f2a7ac 100644
--- a/app/api/admin/quotes/route.ts
+++ b/app/api/admin/quotes/route.ts
@@ -36,7 +36,7 @@ export async function POST(request: Request) {
// 의뢰(contact_requests) 연결용 필드 — string만 허용
const insertData: Record = {
- title: body.title || '새 견적서',
+ title: typeof body.title === 'string' && body.title.trim() ? body.title : '새 견적서',
client_name: typeof body.client_name === 'string' ? body.client_name : '',
client_email: typeof body.client_email === 'string' ? body.client_email : '',
valid_until: body.valid_until || null,