From 70abad31b7eded12cf6037cad1272069b4ed2792 Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 12 Jun 2026 05:23:01 +0900 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=EC=9D=98=EB=A2=B0=E2=86=92?= =?UTF-8?q?=EA=B2=AC=EC=A0=81=20=EC=97=B0=EA=B2=B0=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?+=20=EA=B2=AC=EC=A0=81=20=EB=B0=9C=EC=86=A1(=EB=A9=94=EC=9D=BC?= =?UTF-8?q?=C2=B7=EC=83=81=ED=83=9C=20=EB=8F=99=EA=B8=B0=ED=99=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/contacts/page.tsx | 44 +++++++++++++ app/admin/quotes/[id]/page.tsx | 44 +++++++++++++ app/api/admin/quotes/[id]/send/route.ts | 84 +++++++++++++++++++++++++ app/api/admin/quotes/route.ts | 28 +++++---- 4 files changed, 189 insertions(+), 11 deletions(-) create mode 100644 app/api/admin/quotes/[id]/send/route.ts diff --git a/app/admin/contacts/page.tsx b/app/admin/contacts/page.tsx index e06bf0e..a89b933 100644 --- a/app/admin/contacts/page.tsx +++ b/app/admin/contacts/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; interface Contact { id: string; @@ -29,11 +30,41 @@ const SERVICE_LABELS: Record = { }; export default function AdminContactsPage() { + const router = useRouter(); const [contacts, setContacts] = useState([]); const [loading, setLoading] = useState(true); const [selected, setSelected] = useState(null); const [updating, setUpdating] = useState(null); const [filterStatus, setFilterStatus] = useState('all'); + const [creatingQuote, setCreatingQuote] = useState(false); + + async function createQuote(contact: Contact) { + setCreatingQuote(true); + try { + const title = `${SERVICE_LABELS[contact.service] ?? contact.service ?? '외주 문의'} — ${contact.name ?? ''}`.trim(); + const res = await fetch('/api/admin/quotes', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title, + contact_request_id: contact.id, + client_name: contact.name ?? '', + client_email: contact.email, + }), + }); + const d = await res.json(); + if (res.ok && d.quote?.id) { + router.push('/admin/quotes/' + d.quote.id); + } else { + alert(d.error || '견적서 생성에 실패했습니다'); + } + } catch (e) { + console.error(e); + alert('견적서 생성 중 오류가 발생했습니다'); + } finally { + setCreatingQuote(false); + } + } useEffect(() => { fetch('/api/admin/contacts') @@ -221,6 +252,19 @@ export default function AdminContactsPage() { 이메일 답장하기 + + {/* 견적서 작성 */} + )} diff --git a/app/admin/quotes/[id]/page.tsx b/app/admin/quotes/[id]/page.tsx index cea560e..ef8db76 100644 --- a/app/admin/quotes/[id]/page.tsx +++ b/app/admin/quotes/[id]/page.tsx @@ -64,6 +64,7 @@ export default function QuoteEditorPage() { const [copied, setCopied] = useState(false); const [milestones, setMilestones] = useState([]); const [mileSaving, setMileSaving] = useState(null); + const [sending, setSending] = useState(false); useEffect(() => { fetch(`/api/admin/quotes/${id}`) @@ -125,6 +126,32 @@ export default function QuoteEditorPage() { setMileSaving(null); } + // ── 고객에게 발송 ─────────────────────── + async function sendToClient() { + if (!form.client_email) 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) { + setField('status', 'sent'); + if (d.emailSent === false) { + alert('상태는 변경됐으나 메일 발송에 실패했습니다 — 수동 발송이 필요합니다'); + } else { + alert('발송 완료'); + } + } else { + alert(d.error || '발송에 실패했습니다'); + } + } catch (e) { + console.error(e); + alert('발송 중 오류가 발생했습니다'); + } finally { + setSending(false); + } + } + // ── helpers ──────────────────────────── const setField = (k: keyof QuoteForm, v: unknown) => setForm((f) => ({ ...f, [k]: v })); @@ -255,6 +282,23 @@ export default function QuoteEditorPage() { PDF 저장 )} + {/* 고객에게 발송 */} + + {!form.client_email && ( + 이메일 입력 필요 + )} + {/* 저장 */}