'use client'; import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { REQUEST_STATUS, RequestStatus } from '@/lib/request-status'; interface QuoteSummary { id: string; title: string; status: string; } interface Contact { id: string; email: string; name: string | null; service: string; message: string; status: string; created_at: string; public_token?: string; project_type?: string; budget?: string; timeline?: string; quotes?: QuoteSummary[]; } /** 상태별 색상 매핑 — admin 다크 톤 bg-*-900/40 text-*-400 */ const STATUS_COLORS: Record = { pending: 'bg-yellow-900/40 text-yellow-400', reviewing: 'bg-sky-900/40 text-sky-400', quoted: 'bg-blue-900/40 text-blue-400', accepted: 'bg-green-900/40 text-green-400', in_progress: 'bg-blue-900/40 text-blue-400', completed: 'bg-green-900/40 text-green-400', on_hold: 'bg-slate-700/60 text-slate-400', cancelled: 'bg-red-900/40 text-red-400', }; function getStatusColor(status: string): string { return STATUS_COLORS[status] ?? 'bg-slate-700/60 text-slate-400'; } function getStatusLabel(status: string): string { return (REQUEST_STATUS as Record)[status]?.label ?? status; } const SERVICE_LABELS: Record = { lotto: '로또 추천', stock: '주식 자동매매', automation: '업무 자동화', prompt: '프롬프트 엔지니어링', freelance: '외주 개발', saju: 'AI 사주', general: '일반 문의', }; /** 필터 탭 정의 */ const FILTER_TABS: { val: string; label: string }[] = [ { val: 'all', label: '전체' }, { val: 'pending', label: '접수' }, { val: 'reviewing', label: '검토중' }, { val: 'quoted', label: '견적 발송' }, { val: 'accepted', label: '수주 확정' }, { val: 'in_progress', label: '진행중' }, { val: 'completed', label: '완료' }, { val: '__other', label: '기타' }, ]; const OTHER_STATUSES = new Set(['on_hold', 'cancelled']); function matchFilter(status: string, filterVal: string): boolean { if (filterVal === 'all') return true; if (filterVal === '__other') return OTHER_STATUSES.has(status); return status === filterVal; } function filterCount(contacts: Contact[], filterVal: string): number { if (filterVal === 'all') return contacts.length; return contacts.filter((c) => matchFilter(c.status, filterVal)).length; } 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); const [copied, setCopied] = 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') .then((r) => r.json()) .then((d) => setContacts(d.contacts ?? [])) .catch(console.error) .finally(() => setLoading(false)); }, []); async function updateStatus(id: string, status: string) { setUpdating(id); try { const res = await fetch('/api/admin/contacts', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, status }), }); if (res.ok) { setContacts((prev) => prev.map((c) => (c.id === id ? { ...c, status } : c)) ); if (selected?.id === id) { setSelected((prev) => prev ? { ...prev, status } : null); } } } catch (e) { console.error(e); } finally { setUpdating(null); } } function copyTrackingLink(token: string) { navigator.clipboard.writeText(location.origin + '/track/' + token).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); }); } const filtered = contacts.filter((c) => matchFilter(c.status, filterStatus)); const pendingCount = contacts.filter((c) => c.status === 'pending').length; return (

문의 내역

고객 문의 및 외주 의뢰 관리

{pendingCount > 0 && ( 미처리 {pendingCount}건 )}
{/* 필터 탭 */}
{FILTER_TABS.map(({ val, label }) => ( ))}
{loading ? (
) : (
{/* 목록 */}
{filtered.length === 0 ? (
문의 내역이 없습니다
) : ( filtered.map((contact) => ( )) )}
{/* 상세 패널 */} {selected && (

문의 상세

이름
{selected.name ?? '-'}
이메일
{selected.email}
서비스
{SERVICE_LABELS[selected.service] ?? selected.service}
접수일
{new Date(selected.created_at).toLocaleString('ko-KR')}
내용
{selected.message}
{/* 프로젝트 정보 */} {(selected.project_type || selected.budget || selected.timeline) && (

프로젝트 정보

{selected.project_type && (
유형 {selected.project_type}
)} {selected.budget && (
예산 {selected.budget}
)} {selected.timeline && (
일정 {selected.timeline}
)}
)} {/* 상태 변경 — 8종 select */}

상태 변경

{/* 추적 링크 복사 */} {selected.public_token && ( )} {/* 연결된 견적 */} {selected.quotes && selected.quotes.length > 0 && (

연결된 견적

{selected.quotes.map((q) => ( {q.title} {q.status} ))}
)} {/* 이메일 바로 보내기 링크 */} 이메일 답장하기 {/* 견적서 작성 (연결 견적이 있으면 라벨 변경) */}
)}
)}
); }