import type { Metadata } from 'next'; import { notFound } from 'next/navigation'; import Link from 'next/link'; import { createAdminClient } from '@/lib/supabase/admin'; import { REQUEST_STATUS, TIMELINE_STEPS, timelineIndex, isRequestStatus, type RequestStatus, } from '@/lib/request-status'; // 비회원 의뢰 추적 페이지 (서버 컴포넌트). // 고객이 이메일의 추적 링크로 로그인 없이 의뢰 진행 상태를 확인한다. // PublicShell(TopNav+푸터) 안에서 렌더되므로 여기서는 콘텐츠 섹션만 그린다. // API(app/api/track/[token])와 동일한 조회를 페이지에서 직접 수행한다. // PII(이메일·전화·메시지 본문)는 select에서 제외하며, 모든 DB 예외는 notFound()로 폴백한다. export const dynamic = 'force-dynamic'; export const metadata: Metadata = { title: '의뢰 진행 상태', robots: { index: false, follow: false }, }; const KOR_TIGHT = { letterSpacing: '-0.02em' } as const; const KOR_BODY = { letterSpacing: '-0.01em' } as const; interface Props { params: Promise<{ token: string }>; } interface TrackRequest { id: string; name: string | null; service: string | null; status: string; project_type: string | null; budget: string | null; timeline: string | null; created_at: string; updated_at: string | null; } interface TrackQuote { public_token: string; title: string | null; status: string; valid_until: string | null; } const QUOTE_BADGE: Record = { sent: { label: '확인 대기', tone: 'accent' }, accepted: { label: '수락됨', tone: 'muted' }, rejected: { label: '거절됨', tone: 'danger' }, }; async function loadTrack( token: string, ): Promise<{ request: TrackRequest; quote: TrackQuote | null } | null> { if (!token || token.length > 64) return null; try { const admin = createAdminClient(); const { data: request, error } = await admin .from('contact_requests') .select('id, name, service, status, project_type, budget, timeline, created_at, updated_at') .eq('public_token', token) .maybeSingle(); if (error || !request) return null; const { data: quote } = await admin .from('quotes') .select('public_token, title, status, valid_until') .eq('contact_request_id', request.id) .in('status', ['sent', 'accepted', 'rejected']) .order('created_at', { ascending: false }) .limit(1) .maybeSingle(); return { request: request as TrackRequest, quote: (quote as TrackQuote) ?? null }; } catch (err) { // DB 장애·마이그레이션 미적용(42703 등) — 추적 페이지는 404로 폴백 console.error('[Track] loadTrack failed:', err); return null; } } function fmtDate(value: string | null): string | null { if (!value) return null; const d = new Date(value); if (Number.isNaN(d.getTime())) return null; return d.toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric' }); } function CheckIcon() { return ( ); } function ArrowRight() { return ( ); } /** 진행 단계 타임라인 — 모바일 세로 / 데스크톱 가로 */ function Timeline({ current }: { current: number }) { return (
    {TIMELINE_STEPS.map((step, i) => { const isDone = i < current; const isCurrent = i === current; const isLast = i === TIMELINE_STEPS.length - 1; const label = REQUEST_STATUS[step].label; // 이 단계로 들어오는 연결선이 채워졌는지(이전 단계가 지났는지) const lineFilled = i <= current; return (
  1. {/* 모바일: 세로 마커+연결선 / 데스크톱: 가로 */}
    {/* 데스크톱 좌측 연결선 (가로) */} {i > 0 && ( )} {/* 마커 원 */} {isDone ? ( ) : ( )} {/* 데스크톱 우측 연결선 (가로) */} {!isLast && ( )} {/* 모바일 세로 연결선 */} {!isLast && ( )}
    {/* 라벨 */}
    {label} {isCurrent && ( 진행 중 )}
  2. ); })}
); } export default async function TrackPage({ params }: Props) { const { token } = await params; const data = await loadTrack(token); if (!data) notFound(); const { request, quote } = data; const status: RequestStatus = isRequestStatus(request.status) ? request.status : 'pending'; const current = timelineIndex(status); const receivedAt = fmtDate(request.created_at); const info: { label: string; value: string }[] = []; if (request.project_type) info.push({ label: '프로젝트 유형', value: request.project_type }); if (request.budget) info.push({ label: '예산', value: request.budget }); if (request.timeline) info.push({ label: '희망 일정', value: request.timeline }); const quoteBadge = quote ? QUOTE_BADGE[quote.status] ?? null : null; const quoteValidUntil = quote ? fmtDate(quote.valid_until) : null; return (
{/* ─── 헤더 ─── */}
의뢰 진행 상태

{request.service ?? '의뢰하신 프로젝트'}

{receivedAt && (

{receivedAt} 접수

)}
{/* ─── 진행 상태 ─── */}
{status === 'cancelled' ? (

취소된 의뢰입니다

이 의뢰는 취소 처리되었습니다. 다시 진행을 원하시면 회신해 주세요.

) : ( <> {status === 'on_hold' && (

현재 보류 중입니다 — 조건 조정이 필요하면 회신 주세요.

)} )}
{/* ─── 의뢰 정보 ─── */} {info.length > 0 && (

의뢰 정보

{info.map((item) => (
{item.label}
{item.value}
))}
)} {/* ─── 견적 카드 ─── */} {quote && (

견적서가 도착했습니다

{quote.title ?? '프로젝트 견적서'}

{quoteBadge && ( {quoteBadge.label} )}
{quoteValidUntil && (

유효기간 {quoteValidUntil}까지

)} 견적서 보기
)} {/* ─── 하단 안내 ─── */}

문의사항은{' '} bgg8988@gmail.com {' '} 또는 접수하신 메일에 회신해 주세요.

); }