diff --git a/app/mypage/page.tsx b/app/mypage/page.tsx index fdca5d1..090cb53 100644 --- a/app/mypage/page.tsx +++ b/app/mypage/page.tsx @@ -7,6 +7,13 @@ import { createClient } from '@/lib/supabase/client'; import type { User } from '@supabase/supabase-js'; import TelegramGuideModal from '@/app/components/TelegramGuideModal'; import { KAKAO_OPENCHAT_URL } from '@/lib/contact'; +import { + REQUEST_STATUS, + TIMELINE_STEPS, + timelineIndex, + isRequestStatus, + type RequestStatus, +} from '@/lib/request-status'; // 마이페이지 — 4탭 재구성 (프로필 / 내 의뢰 / 내 제품 / 주문 내역). // PublicShell(TopNav)이 상단 내비·로그아웃을 제공하므로 여기서는 콘텐츠만 렌더한다. @@ -53,6 +60,12 @@ interface Order { service: string; message: string; status: string; + // 2026-06-12-client-portal 마이그레이션 신규 컬럼 — 미적용 환경에선 undefined + public_token?: string | null; + project_type?: string | null; + budget?: string | null; + timeline?: string | null; + updated_at?: string | null; } // 구매 제품 자료 그룹 (/api/packs/list-mine 응답) @@ -86,6 +99,8 @@ function MyPageContent() { const [productGroups, setProductGroups] = useState([]); const [productOrders, setProductOrders] = useState([]); const [downloading, setDownloading] = useState(null); + // 내 의뢰 탭 — 펼친 카드 id 집합 (기본 접힘) + const [expandedRequests, setExpandedRequests] = useState>(new Set()); // 텔레그램 연동 상태 const [telegramChatId, setTelegramChatId] = useState(null); @@ -195,6 +210,15 @@ function MyPageContent() { setTelegramLinkState('idle'); }; + function toggleRequest(id: string) { + setExpandedRequests((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + } + async function handleDownload(fileId: string) { setDownloading(fileId); try { @@ -503,23 +527,12 @@ function MyPageContent() { ) : (
{orders.map((o) => ( - -
-
- {o.service} -
- -
-

- {o.message} -

-
- {new Date(o.created_at).toLocaleDateString('ko-KR')} -
-
+ toggleRequest(o.id)} + /> ))}
)} @@ -792,27 +805,261 @@ function QuickLink({ href, title, sub }: { href: string; title: string; sub: str ); } -// 상태 뱃지 — pending=surface-alt / in_progress=accent-soft / completed=성공 그린(예외 허용) +// 상태 뱃지 — REQUEST_STATUS 8종. +// completed=성공 그린(예외 허용) / accepted·quoted·in_progress=accent / pending·reviewing=surface-alt +// on_hold·cancelled=faint. 알 수 없는 값(다른 도메인 status 등)은 원문 라벨+기본 스타일 폴백. +const STATUS_BADGE_STYLE: Record = { + completed: { background: '#dcfce7', color: '#166534' }, + accepted: { background: 'var(--jsm-accent-soft)', color: 'var(--jsm-accent)' }, + in_progress: { background: 'var(--jsm-accent-soft)', color: 'var(--jsm-accent)' }, + quoted: { background: 'var(--jsm-accent-soft)', color: 'var(--jsm-accent)' }, + pending: { background: 'var(--jsm-surface-alt)', color: 'var(--jsm-ink-soft)' }, + reviewing: { background: 'var(--jsm-surface-alt)', color: 'var(--jsm-ink-soft)' }, + on_hold: { background: 'var(--jsm-surface-alt)', color: 'var(--jsm-ink-faint)' }, + cancelled: { background: 'var(--jsm-surface-alt)', color: 'var(--jsm-ink-faint)' }, +}; + function StatusBadge({ status }: { status: string }) { - const map: Record = { - completed: { label: '완료', style: { background: '#dcfce7', color: '#166534' } }, - in_progress: { label: '진행중', style: { background: 'var(--jsm-accent-soft)', color: 'var(--jsm-accent)' } }, - pending: { label: '대기중', style: { background: 'var(--jsm-surface-alt)', color: 'var(--jsm-ink-soft)' } }, - }; - const conf = map[status] ?? { - label: status, - style: { background: 'var(--jsm-surface-alt)', color: 'var(--jsm-ink-soft)' }, - }; + const known = isRequestStatus(status); + const label = known ? REQUEST_STATUS[status].label : status; + const style = known + ? STATUS_BADGE_STYLE[status] + : { background: 'var(--jsm-surface-alt)', color: 'var(--jsm-ink-soft)' }; return ( - {conf.label} + {label} ); } +// 펼침 토글 셰브론 +function Chevron({ open }: { open: boolean }) { + return ( + + + + ); +} + +function TimelineCheck() { + return ( + + + + ); +} + +// 컴팩트 가로 미니 타임라인 — track 페이지 타임라인의 축소판. +// 모바일에서는 라벨을 숨기고 도트만 노출(라벨 축약 허용). +function MiniTimeline({ 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; + return ( +
  1. +
    + {/* 좌측 연결선 */} + + {/* 마커 */} + + {isDone ? ( + + ) : ( + + )} + + {/* 우측 연결선 */} + +
    + {/* 라벨 — 모바일 숨김 */} + + {label} + +
  2. + ); + })} +
+ ); +} + +// 내 의뢰 카드 — 접힘 기본, 펼치면 타임라인 + 의뢰 정보 + 추적 링크 +function RequestCard({ + order, + expanded, + onToggle, +}: { + order: Order; + expanded: boolean; + onToggle: () => void; +}) { + const status: RequestStatus = isRequestStatus(order.status) ? order.status : 'pending'; + const current = timelineIndex(status); + + const info: { label: string; value: string }[] = []; + if (order.project_type) info.push({ label: '프로젝트 유형', value: order.project_type }); + if (order.budget) info.push({ label: '예산', value: order.budget }); + if (order.timeline) info.push({ label: '희망 일정', value: order.timeline }); + + return ( + + {/* 헤더 — 클릭 토글 */} + + + {/* 펼침 영역 */} + {expanded && ( +
+ {status === 'cancelled' ? ( +

+ 취소된 의뢰입니다. +

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

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

+
+ )} +
+ +
+ + )} + + {/* 의뢰 정보 */} + {info.length > 0 && ( +
+ {info.map((item) => ( +
+
+ {item.label} +
+
+ {item.value} +
+
+ ))} +
+ )} + + {/* 상세 추적 페이지 링크 */} + {order.public_token && ( + + 상세 추적 페이지 + + + )} +
+ )} +
+ ); +} + function EmptyState({ title, desc,