fix(admin): 견적 재발송 방어 + title 타입 검증

- POST /api/admin/quotes: title을 typeof + trim() 검증으로 falsy 문자열 방어
- POST /api/admin/quotes/[id]/send: sent/accepted/rejected 상태면 200 조기 반환(alreadySent: true)으로 중복 발송 차단
- 견적 편집 UI: isSentStatus 플래그로 발송 버튼 비활성화·라벨 "발송됨" 표시, alreadySent 응답 시 안내 alert 처리

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 05:28:22 +09:00
parent 70abad31b7
commit 5ceae7e90b
3 changed files with 27 additions and 11 deletions

View File

@@ -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<string, unknown> = { 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({

View File

@@ -36,7 +36,7 @@ export async function POST(request: Request) {
// 의뢰(contact_requests) 연결용 필드 — string만 허용
const insertData: Record<string, unknown> = {
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,