diff --git a/app/mypage/page.tsx b/app/mypage/page.tsx index 090cb53..f4a5d29 100644 --- a/app/mypage/page.tsx +++ b/app/mypage/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Suspense, useEffect, useState } from 'react'; +import { Suspense, useCallback, useEffect, useState } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import Link from 'next/link'; import { createClient } from '@/lib/supabase/client'; @@ -87,6 +87,15 @@ interface ProductOrder { created_at: string; } +// 발주·진행 (quotes 기반 — 견적 수락 시 발주서로 전환, /api/projects) +type ProjectMilestone = { quote_id: string; step_number: number; title: string; status: 'pending' | 'in_progress' | 'completed' }; +type Project = { id: string; title: string; status: string; total: number; created_at: string; milestones: ProjectMilestone[] }; + +const QUOTE_STATUS_LABELS: Record = { + sent: '견적 발송', accepted: '발주 확정', in_progress: '진행중', completed: '완료', delivered: '납품 완료', +}; +const PROJECT_ORDERED_STATUSES = ['accepted', 'in_progress', 'completed', 'delivered']; + function MyPageContent() { const router = useRouter(); const searchParams = useSearchParams(); @@ -102,6 +111,13 @@ function MyPageContent() { // 내 의뢰 탭 — 펼친 카드 id 집합 (기본 접힘) const [expandedRequests, setExpandedRequests] = useState>(new Set()); + // 발주·진행 (quotes 기반) + const [projects, setProjects] = useState([]); + const [linkCode, setLinkCode] = useState(''); + const [linkMsg, setLinkMsg] = useState(null); + const [linking, setLinking] = useState(false); + const [showLinkForm, setShowLinkForm] = useState(false); + // 텔레그램 연동 상태 const [telegramChatId, setTelegramChatId] = useState(null); const [telegramLinkState, setTelegramLinkState] = useState('idle'); @@ -109,6 +125,15 @@ function MyPageContent() { const [telegramLinkExpiry, setTelegramLinkExpiry] = useState(''); const [showTelegramGuide, setShowTelegramGuide] = useState(false); + const loadProjects = useCallback(async () => { + try { + const res = await fetch('/api/projects'); + if (!res.ok) return; + const d = await res.json(); + setProjects(d.projects ?? []); + } catch { /* 미로그인/네트워크 — 무시 */ } + }, []); + useEffect(() => { async function init() { const { data: { user } } = await supabase.auth.getUser(); @@ -160,10 +185,13 @@ function MyPageContent() { .limit(50); setProductOrders(prodOrders || []); + // 발주·진행 (quotes 기반) 조회 + await loadProjects(); + setLoading(false); } init(); - }, []); + }, [loadProjects]); // ── 텔레그램 연결 ── const handleTelegramConnect = async () => { @@ -219,6 +247,24 @@ function MyPageContent() { }); } + // 견적서 코드 연결 (공개 견적 페이지에서 발급된 public_token) + const handleLink = async () => { + if (!linkCode.trim() || linking) return; + setLinking(true); setLinkMsg(null); + try { + const res = await fetch('/api/projects/link', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token: linkCode.trim() }), + }); + const d = await res.json(); + if (!res.ok) { setLinkMsg(d.error ?? '연결에 실패했습니다.'); return; } + setLinkMsg(d.alreadyLinked ? '이미 연결된 견적서입니다.' : '견적서가 연결되었습니다.'); + setLinkCode(''); + await loadProjects(); + } catch { setLinkMsg('연결에 실패했습니다. 다시 시도해주세요.'); } + finally { setLinking(false); } + }; + async function handleDownload(fileId: string) { setDownloading(fileId); try { @@ -260,7 +306,7 @@ function MyPageContent() { const tabs: { key: Tab; label: string; count?: number }[] = [ { key: 'profile', label: '프로필' }, - { key: 'requests', label: '내 의뢰', count: orders.length || undefined }, + { key: 'requests', label: '발주·진행', count: orders.length || undefined }, { key: 'products', label: '내 제품', count: productGroups.length || undefined }, { key: 'orders', label: '주문 내역', count: (orders.length + payments.length) || undefined }, ]; @@ -514,9 +560,71 @@ function MyPageContent() { )} - {/* ===== 내 의뢰 ===== */} + {/* ===== 발주·진행 + 내 의뢰 ===== */} {tab === 'requests' && (
+ {/* 발주·진행 (quotes 기반 — 견적 수락 시 발주서로 전환) */} +
+
+

+ 발주·진행 +

+ +
+ + {showLinkForm && ( +
+
+ setLinkCode(e.target.value)} + placeholder="견적서 코드를 입력하세요" + className="flex-1 min-w-0 px-3 py-2 rounded-lg border text-sm" + style={{ borderColor: 'var(--jsm-line)', color: 'var(--jsm-ink)' }} + /> + +
+ {linkMsg && ( +

+ {linkMsg} +

+ )} +
+ )} + + {projects.length === 0 ? ( +

+ 진행 중인 발주가 없습니다. 견적서 코드를 입력해 연결하거나 새로 의뢰해 보세요. +

+ ) : ( +
+ {projects.map((p) => ( + + ))} +
+ )} +
+ + {/* 기존 의뢰 카드 리스트 (contact_requests 기반) */} {orders.length === 0 ? ( +
+
+ {project.title} +
+
+ {isProjectOrder(project.status) && ( + + 발주서 + + )} + + {QUOTE_STATUS_LABELS[project.status] ?? project.status} + +
+
+ +
+ {project.total.toLocaleString('ko-KR')}원 +
+ + {project.milestones.length > 0 && ( +
    + {project.milestones + .slice() + .sort((a, b) => a.step_number - b.step_number) + .map((m) => { + const done = m.status === 'completed'; + const active = m.status === 'in_progress'; + return ( +
  1. + + {m.step_number} + + + {m.title} + +
  2. + ); + })} +
+ )} + + ); +} + function EmptyState({ title, desc,