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,