'use client'; import { useEffect, useState, useCallback } from 'react'; import { useParams, useRouter } from 'next/navigation'; import Link from 'next/link'; /* ─── 타입 ─────────────────────────────────────────────── */ 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 QuoteForm { title: string; client_name: string; client_email: string; valid_until: string; status: string; wbs: WBSPhase[]; items: QuoteItem[]; maintenance: MaintenancePlan[]; notes: string; } const newId = () => Math.random().toString(36).slice(2, 9); const STATUS_OPTIONS = [ { value: 'draft', label: '초안' }, { value: 'sent', label: '발송됨' }, { value: 'accepted', label: '수락됨' }, { value: 'rejected', label: '거절됨' }, ]; const ITEM_CATEGORIES = ['기획', '디자인', '개발', '인프라', '유지보수', '기타']; const TABS = ['기본정보', 'WBS', '견적항목', '향후관리', '특이사항', '진행 단계'] as const; type Tab = typeof TABS[number]; interface Milestone { id: string; step_number: number; title: string; description: string; status: 'pending' | 'in_progress' | 'completed'; note: string; completed_at: string | null; } /* ─── 컴포넌트 ─────────────────────────────────────────── */ export default function QuoteEditorPage() { const params = useParams(); const router = useRouter(); const id = params.id as string; const [tab, setTab] = useState('기본정보'); const [form, setForm] = useState({ title: '새 견적서', client_name: '', client_email: '', valid_until: '', status: 'draft', wbs: [], items: [], maintenance: [], notes: '', }); const [publicToken, setPublicToken] = useState(''); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const [copied, setCopied] = useState(false); const [milestones, setMilestones] = useState([]); const [mileSaving, setMileSaving] = useState(null); useEffect(() => { fetch(`/api/admin/quotes/${id}`) .then((r) => r.json()) .then((d) => { if (d.quote) { const q = d.quote; setForm({ title: q.title, client_name: q.client_name, client_email: q.client_email, valid_until: q.valid_until?.slice(0, 10) ?? '', status: q.status, wbs: q.wbs ?? [], items: q.items ?? [], maintenance: q.maintenance ?? [], notes: q.notes ?? '', }); setPublicToken(q.public_token); } }) .finally(() => setLoading(false)); }, [id]); const save = useCallback(async (silent = false) => { if (!silent) setSaving(true); await fetch(`/api/admin/quotes/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form), }); if (!silent) { setSaving(false); setSaved(true); setTimeout(() => setSaved(false), 2000); } }, [id, form]); // ── Milestones ────────────────────────── async function fetchMilestones() { const res = await fetch(`/api/admin/milestones?quoteId=${id}`); const d = await res.json(); setMilestones(d.milestones ?? []); } async function initDefaultMilestones() { if (!confirm('기존 단계를 삭제하고 기본 7단계로 초기화할까요?')) return; const res = await fetch('/api/admin/milestones', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ useDefaults: true, quoteId: id }), }); const d = await res.json(); setMilestones(d.milestones ?? []); } async function updateMilestone(mid: string, field: string, value: string) { setMileSaving(mid); const res = await fetch(`/api/admin/milestones/${mid}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ [field]: value }), }); const d = await res.json(); if (d.milestone) { setMilestones((prev) => prev.map((m) => m.id === mid ? d.milestone : m)); } setMileSaving(null); } // ── helpers ──────────────────────────── const setField = (k: keyof QuoteForm, v: unknown) => setForm((f) => ({ ...f, [k]: v })); const totalPrice = form.items.reduce((s, i) => s + i.unitPrice * i.quantity, 0); function copyLink() { navigator.clipboard.writeText(`${window.location.origin}/quote/${publicToken}`); setCopied(true); setTimeout(() => setCopied(false), 2000); } // ── WBS ──────────────────────────────── function addPhase() { setField('wbs', [...form.wbs, { id: newId(), phase: '새 단계', tasks: [] }]); } function updatePhase(phaseId: string, k: string, v: string) { setField('wbs', form.wbs.map((p) => p.id === phaseId ? { ...p, [k]: v } : p)); } function removePhase(phaseId: string) { setField('wbs', form.wbs.filter((p) => p.id !== phaseId)); } function addTask(phaseId: string) { setField('wbs', form.wbs.map((p) => p.id === phaseId ? { ...p, tasks: [...p.tasks, { id: newId(), name: '새 작업', duration: '1일', description: '' }] } : p)); } function updateTask(phaseId: string, taskId: string, k: string, v: string) { setField('wbs', form.wbs.map((p) => p.id === phaseId ? { ...p, tasks: p.tasks.map((t) => t.id === taskId ? { ...t, [k]: v } : t) } : p)); } function removeTask(phaseId: string, taskId: string) { setField('wbs', form.wbs.map((p) => p.id === phaseId ? { ...p, tasks: p.tasks.filter((t) => t.id !== taskId) } : p)); } // ── Items ─────────────────────────────── function addItem() { setField('items', [...form.items, { id: newId(), category: '개발', name: '', description: '', quantity: 1, unitPrice: 0, optional: false, }]); } function updateItem(itemId: string, k: string, v: unknown) { setField('items', form.items.map((i) => i.id === itemId ? { ...i, [k]: v } : i)); } function removeItem(itemId: string) { setField('items', form.items.filter((i) => i.id !== itemId)); } // ── Maintenance ───────────────────────── function addPlan() { setField('maintenance', [...form.maintenance, { id: newId(), name: '기본 유지보수', period: '3개월', monthlyFee: 0, includes: ['버그 수정', '소소한 변경'], recommended: false, }]); } function updatePlan(planId: string, k: string, v: unknown) { setField('maintenance', form.maintenance.map((p) => p.id === planId ? { ...p, [k]: v } : p)); } function removePlan(planId: string) { setField('maintenance', form.maintenance.filter((p) => p.id !== planId)); } function updatePlanInclude(planId: string, idx: number, v: string) { setField('maintenance', form.maintenance.map((p) => p.id === planId ? { ...p, includes: p.includes.map((inc, i) => i === idx ? v : inc) } : p)); } function addPlanInclude(planId: string) { setField('maintenance', form.maintenance.map((p) => p.id === planId ? { ...p, includes: [...p.includes, ''] } : p)); } function removePlanInclude(planId: string, idx: number) { setField('maintenance', form.maintenance.map((p) => p.id === planId ? { ...p, includes: p.includes.filter((_, i) => i !== idx) } : p)); } if (loading) { return
불러오는 중...
; } return (
{/* 상단 바 */}

{form.title || '견적서 편집'}

{form.client_name || '고객 미지정'} · 합계 {totalPrice.toLocaleString()}원

{/* 공개 링크 */} {publicToken && ( )} {/* 미리보기 */} {publicToken && ( 미리보기 )} {/* PDF 저장 */} {publicToken && ( PDF 저장 )} {/* 저장 */}
{/* 탭 */}
{TABS.map((t) => ( ))}
{/* 콘텐츠 */}
{/* ── 기본정보 ── */} {tab === '기본정보' && (
setField('title', e.target.value)} placeholder="예: 쇼핑몰 개발 견적서 v1.0" />
setField('client_name', e.target.value)} placeholder="홍길동" /> setField('client_email', e.target.value)} placeholder="client@example.com" />
setField('valid_until', e.target.value)} />
{/* 요약 카드 */}

견적 요약

{form.items.length}
총 항목
{form.items.filter(i => !i.optional).length}
필수 항목
{form.items.filter(i => i.optional).length}
선택 항목
총 견적 금액 {totalPrice.toLocaleString()}원
)} {/* ── WBS ── */} {tab === 'WBS' && (

작업 분류 체계(WBS)를 단계별로 작성합니다

{form.wbs.length === 0 && ( )} {form.wbs.map((phase, pi) => (
{pi + 1} updatePhase(phase.id, 'phase', e.target.value)} placeholder="단계명 (예: 기획, 디자인, 개발)" />
{phase.tasks.length > 0 && (
{phase.tasks.map((task) => (
updateTask(phase.id, task.id, 'name', e.target.value)} placeholder="작업명" />
updateTask(phase.id, task.id, 'duration', e.target.value)} placeholder="기간 (예: 3일)" />
updateTask(phase.id, task.id, 'description', e.target.value)} placeholder="작업 설명" />
))}
)} {phase.tasks.length === 0 && (

작업 없음 — 위 버튼으로 추가하세요

)}
))}
)} {/* ── 견적항목 ── */} {tab === '견적항목' && (

선택 항목(optional)은 고객이 직접 선택/해제할 수 있습니다

{/* 헤더 */} {form.items.length > 0 && (
카테고리
항목명
설명
수량
단가
선택
)} {form.items.length === 0 && } {form.items.map((item) => (
updateItem(item.id, 'name', e.target.value)} placeholder="항목명" />
updateItem(item.id, 'description', e.target.value)} placeholder="상세 설명" />
updateItem(item.id, 'quantity', Number(e.target.value))} />
updateItem(item.id, 'unitPrice', Number(e.target.value))} />
))} {/* 합계 */} {form.items.length > 0 && (
필수 합계 {form.items.filter(i => !i.optional).reduce((s, i) => s + i.unitPrice * i.quantity, 0).toLocaleString()}원
선택 합계 {form.items.filter(i => i.optional).reduce((s, i) => s + i.unitPrice * i.quantity, 0).toLocaleString()}원
전체 합계 {totalPrice.toLocaleString()}원
)}
)} {/* ── 향후관리 ── */} {tab === '향후관리' && (

납품 후 유지보수 플랜을 구성합니다 (고객이 하나를 선택)

{form.maintenance.length === 0 && } {form.maintenance.map((plan) => (
updatePlan(plan.id, 'name', e.target.value)} placeholder="기본 유지보수" /> updatePlan(plan.id, 'period', e.target.value)} placeholder="3개월" /> updatePlan(plan.id, 'monthlyFee', Number(e.target.value))} />
추천
포함 사항
{plan.includes.map((inc, idx) => (
updatePlanInclude(plan.id, idx, e.target.value)} placeholder="포함 사항 입력" />
))}
))}
)} {/* ── 특이사항 ── */} {tab === '특이사항' && (