'use client'; import { useEffect, useState } from 'react'; import { useParams, useSearchParams } from 'next/navigation'; interface WBSTask { id: string; name: string; duration: string; description: string; } interface WBSPhase { id: string; phase: string; tasks: WBSTask[]; } interface QuoteItem { id: string; category: string; name: string; description: string; quantity: number; unitPrice: number; optional: boolean; } interface MaintenancePlan { id: string; name: string; period: string; monthlyFee: number; includes: string[]; recommended: boolean; } interface Quote { id: string; title: string; client_name: string; valid_until: string | null; status: string; wbs: WBSPhase[]; items: QuoteItem[]; maintenance: MaintenancePlan[]; notes: string; created_at: string; } const CATEGORY_COLORS: Record = { 기획: '#60a5fa', 디자인: '#f472b6', 개발: '#34d399', 인프라: '#fb923c', 유지보수: '#a78bfa', 기타: '#94a3b8', }; export default function QuotePage() { const { token } = useParams<{ token: string }>(); const searchParams = useSearchParams(); const [quote, setQuote] = useState(null); const [loading, setLoading] = useState(true); const [notFound, setNotFound] = useState(false); // 선택 상태 const [checkedOptional, setCheckedOptional] = useState>({}); const [selectedMaintenance, setSelectedMaintenance] = useState(null); const [activeTab, setActiveTab] = useState<'overview' | 'wbs' | 'quote' | 'maintenance'>('overview'); const [submitting, setSubmitting] = useState(false); const [submitted, setSubmitted] = useState(false); useEffect(() => { fetch(`/api/quote/${token}`) .then((r) => r.ok ? r.json() : Promise.reject()) .then((d) => { setQuote(d.quote); // 기본값: 필수 항목은 항상 체크, 선택 항목은 기본 체크 const init: Record = {}; (d.quote.items as QuoteItem[]).forEach((i) => { init[i.id] = true; }); setCheckedOptional(init); // 추천 유지보수 플랜 기본 선택 const rec = (d.quote.maintenance as MaintenancePlan[]).find((p) => p.recommended); if (rec) setSelectedMaintenance(rec.id); else if (d.quote.maintenance.length > 0) setSelectedMaintenance(d.quote.maintenance[0].id); // ?print=1 파라미터 시 자동 인쇄 다이얼로그 if (searchParams.get('print') === '1') { setTimeout(() => window.print(), 800); } }) .catch(() => setNotFound(true)) .finally(() => setLoading(false)); }, [token]); const requiredItems = quote?.items.filter((i) => !i.optional) ?? []; const optionalItems = quote?.items.filter((i) => i.optional) ?? []; const requiredTotal = requiredItems.reduce((s, i) => s + i.unitPrice * i.quantity, 0); const optionalTotal = optionalItems .filter((i) => checkedOptional[i.id]) .reduce((s, i) => s + i.unitPrice * i.quantity, 0); const selectedPlan = quote?.maintenance.find((p) => p.id === selectedMaintenance); const maintenanceTotal = selectedPlan ? selectedPlan.monthlyFee : 0; const grandTotal = requiredTotal + optionalTotal; async function handleAccept() { if (!quote) return; setSubmitting(true); const selectedItems = quote.items.filter((i) => !i.optional || checkedOptional[i.id]).map((i) => i.id); await fetch(`/api/quote/${token}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ selectedItems, selectedMaintenance, total: grandTotal }), }); setSubmitting(false); setSubmitted(true); } if (loading) { return (

견적서를 불러오는 중...

); } if (notFound || !quote) { return (
🔍

견적서를 찾을 수 없습니다

링크가 만료되었거나 잘못된 주소입니다

); } if (submitted) { return (
🎉

견적을 수락해 주셨습니다!

담당자가 확인 후 빠른 시일 내에 연락드리겠습니다.
선택하신 내용을 기반으로 계약을 진행합니다.

최종 견적 금액
{grandTotal.toLocaleString()}원
{maintenanceTotal > 0 && (
+ 유지보수 {maintenanceTotal.toLocaleString()}원/월
)}
); } const tabs = [ { key: 'overview', label: '개요' }, { key: 'wbs', label: 'WBS', show: quote.wbs.length > 0 }, { key: 'quote', label: '견적 항목', show: quote.items.length > 0 }, { key: 'maintenance', label: '향후 관리', show: quote.maintenance.length > 0 }, ].filter((t) => t.show !== false); return (
{/* 헤더 */}
{/* 브랜드 */}
쟁승메이드
jaengseung-made.com
공식 견적서
{/* 제목 */}

{quote.title}

{quote.client_name && (
👤 {quote.client_name} 고객님
)} {quote.valid_until && (
📅 유효기간: {quote.valid_until.slice(0, 10)}
)}
📄 발행일: {new Date(quote.created_at).toLocaleDateString('ko-KR')}
{/* 탭 */}
{tabs.map((t) => ( ))}
{/* 본문 */}
{/* ── 개요 ── */} {activeTab === 'overview' && (
checkedOptional[i.id]).length + '개 선택됨'} color="#f59e0b" />
)} {/* ── WBS ── */} {activeTab === 'wbs' && (
{quote.wbs.map((phase, pi) => (
{pi + 1}

{phase.phase}

{phase.tasks.map((task) => ( ))}
작업명 기간 설명
{task.name} {task.duration} {task.description || '—'}
))}
)} {/* ── 견적 항목 ── */} {activeTab === 'quote' && (
{/* 필수 항목 */} {requiredItems.length > 0 && (

필수 항목

{requiredItems.map((item) => ( ))}
카테고리 항목명 설명 수량 단가 금액
{item.category} {item.name} {item.description || '—'} {item.quantity} {item.unitPrice.toLocaleString()} {(item.unitPrice * item.quantity).toLocaleString()}원
)} {/* 선택 항목 */} {optionalItems.length > 0 && (

선택 항목

아래 항목 중 원하시는 것을 선택하세요 — 총 금액에 실시간으로 반영됩니다

{optionalItems.map((item) => ( setCheckedOptional((prev) => ({ ...prev, [item.id]: !prev[item.id] }))} style={{ borderBottom: '1px solid rgba(255,255,255,0.04)', cursor: 'pointer', background: checkedOptional[item.id] ? 'rgba(167,139,250,0.05)' : 'transparent', transition: 'background 0.2s' }}> ))}
선택 카테고리 항목명 설명 수량 금액
{}} /> {item.category} {item.name} {item.description || '—'} {item.quantity} {(item.unitPrice * item.quantity).toLocaleString()}원
)} {/* 합계 */}
필수 항목 {requiredTotal.toLocaleString()}원
{optionalTotal > 0 && (
선택 항목 +{optionalTotal.toLocaleString()}원
)}
합계 (VAT 별도) {grandTotal.toLocaleString()}원
)} {/* ── 향후 관리 ── */} {activeTab === 'maintenance' && (

납품 후 유지보수 플랜을 선택해주세요 (선택 사항)

{quote.maintenance.map((plan) => { const isSelected = selectedMaintenance === plan.id; return (
setSelectedMaintenance(isSelected ? null : plan.id)} style={{ background: isSelected ? 'linear-gradient(135deg, rgba(99,102,241,0.15), rgba(139,92,246,0.1))' : '#0f172a', border: `1px solid ${isSelected ? '#6366f1' : 'rgba(255,255,255,0.06)'}`, borderRadius: 16, padding: 24, cursor: 'pointer', transition: 'all 0.25s', position: 'relative', }}> {plan.recommended && (
추천
)}
{}} />
{plan.name}
{plan.period}
{plan.monthlyFee === 0 ? '무료' : plan.monthlyFee.toLocaleString() + '원/월'}
{plan.includes.map((inc, i) => (
{inc}
))}
); })}
)} {/* 특이사항 */} {quote.notes && (

특이사항 및 참고사항

{quote.notes}

)}
{/* 하단 고정 바 — 견적 수락 */} {quote.status !== 'accepted' && quote.status !== 'rejected' && (
현재 선택된 견적 합계
{grandTotal.toLocaleString()}원 {maintenanceTotal > 0 && selectedPlan && ( + {maintenanceTotal.toLocaleString()}원/월 ({selectedPlan.name}) )}
)} {/* 수락된 경우 */} {quote.status === 'accepted' && (

✓ 이미 수락된 견적서입니다

)} {/* 하단 여백 */}
); } function StatCard({ label, value, sub, color }: { label: string; value: string; sub: string; color: string }) { return (
{label}
{value}
{sub}
); } const thStyle: React.CSSProperties = { padding: '12px 16px', textAlign: 'left', fontSize: 11, fontWeight: 600, color: '#475569', textTransform: 'uppercase', letterSpacing: '0.08em' }; const tdStyle: React.CSSProperties = { padding: '14px 16px', fontSize: 14, color: '#94a3b8' };