From 4bd5400406a2f641cd1aaada37f06df6b9e087d1 Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 11 Jun 2026 02:31:40 +0900 Subject: [PATCH] =?UTF-8?q?feat(mypage):=204=ED=83=AD=20=EC=9E=AC=EA=B5=AC?= =?UTF-8?q?=EC=84=B1=20+=20=EC=A0=84=EB=AC=B8=20=ED=86=A4=20=EB=A6=AC?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20(=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=C2=B7=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EB=AC=B4=EC=88=98=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- app/mypage/page.tsx | 1458 ++++++++++++++++++------------------------- 1 file changed, 595 insertions(+), 863 deletions(-) diff --git a/app/mypage/page.tsx b/app/mypage/page.tsx index c631241..cc0d12a 100644 --- a/app/mypage/page.tsx +++ b/app/mypage/page.tsx @@ -1,7 +1,7 @@ 'use client'; -import { useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; +import { Suspense, useEffect, useState } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; import Link from 'next/link'; import { createClient } from '@/lib/supabase/client'; import type { User } from '@supabase/supabase-js'; @@ -10,29 +10,35 @@ import { PACK_TIER_NAMES, extractPackTier, type PackTier } from '@/lib/pack-asse import type { PackFile } from '@/lib/supabase/pack-files'; import { KAKAO_OPENCHAT_URL } from '@/lib/contact'; -function buildSajuResultUrl(rec: SajuRecord) { - const { birth_year, birth_month, birth_day, birth_hour, gender } = rec.saju_data; - if (!birth_year || !birth_month || !birth_day) return '/work/saju/input'; - let url = `/work/saju/result?year=${birth_year}&month=${birth_month}&day=${birth_day}&gender=${gender}&calendarType=solar`; - if (birth_hour != null) url += `&hour=${birth_hour}`; - return url; -} +// 마이페이지 — 4탭 재구성 (프로필 / 내 의뢰 / 내 제품 / 주문 내역). +// PublicShell(TopNav)이 상단 내비·로그아웃을 제공하므로 여기서는 콘텐츠만 렌더한다. +// 디자인은 메인(/)·외주(/outsourcing) 페이지의 --jsm-* 토큰·타이포 패턴과 일관되게 구성한다. -type Tab = 'profile' | 'projects' | 'subscription' | 'saju' | 'payments' | 'orders' | 'packs'; +const KOR_TIGHT = { letterSpacing: '-0.02em' } as const; +const KOR_BODY = { letterSpacing: '-0.01em' } as const; + +type Tab = 'profile' | 'requests' | 'products' | 'orders'; type TelegramLinkState = 'idle' | 'generating' | 'waiting' | 'disconnecting'; -interface SajuRecord { - id: number; - created_at: string; - saju_data: { - birth_year: number; - birth_month: number; - birth_day: number; - birth_hour?: number; - gender: string; - }; - interpretation: string | null; - is_paid: boolean; +// 구 탭 키 → 새 탭 키 매핑. 사주/구독/프로젝트 등 폐지 탭은 프로필로 폴백. +function resolveTab(raw: string | null): Tab { + switch (raw) { + case 'requests': + return 'requests'; + case 'products': + case 'packs': + return 'products'; + case 'orders': + case 'payments': + return 'orders'; + case 'profile': + case 'saju': + case 'subscription': + case 'projects': + return 'profile'; + default: + return 'requests'; + } } interface Payment { @@ -51,49 +57,15 @@ interface Order { status: string; } -interface ProjectMilestone { - id: string; - step_number: number; - title: string; - description: string; - status: 'pending' | 'in_progress' | 'completed'; - note: string; - completed_at: string | null; -} - -interface Project { - id: string; - title: string; - status: string; - total: number; - created_at: string; - milestones: ProjectMilestone[]; -} - -interface ActiveSubscription { - id: string; - product_id: string; - status: string; - auto_renew: boolean; - started_at: string; - expires_at: string; - cancelled_at: string | null; -} - -export default function MyPage() { +function MyPageContent() { const router = useRouter(); + const searchParams = useSearchParams(); const supabase = createClient(); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); - const [tab, setTab] = useState('projects'); - const [sajuRecords, setSajuRecords] = useState([]); + const [tab, setTab] = useState(() => resolveTab(searchParams.get('tab'))); const [payments, setPayments] = useState([]); const [orders, setOrders] = useState([]); - const [activeSubscriptions, setActiveSubscriptions] = useState([]); - const [projects, setProjects] = useState([]); - const [linkToken, setLinkToken] = useState(''); - const [linking, setLinking] = useState(false); - const [linkMessage, setLinkMessage] = useState(''); const [packFiles, setPackFiles] = useState([]); const [downloading, setDownloading] = useState(null); @@ -113,15 +85,6 @@ export default function MyPage() { } setUser(user); - // 사주 기록 조회 (테이블 있을 때 동작) - const { data: saju } = await supabase - .from('saju_records') - .select('*') - .eq('user_id', user.id) - .order('created_at', { ascending: false }) - .limit(20); - setSajuRecords(saju || []); - // 결제 내역 조회 const { data: pay } = await supabase .from('payments') @@ -148,20 +111,6 @@ export default function MyPage() { .maybeSingle(); setTelegramChatId(profile?.telegram_chat_id ?? null); - // 구독 목록 조회 (subscriptions 테이블) - const subRes = await fetch('/api/subscription'); - if (subRes.ok) { - const subData = await subRes.json(); - setActiveSubscriptions(subData.subscriptions ?? []); - } - - // 프로젝트 진행 현황 조회 - const projRes = await fetch('/api/projects'); - if (projRes.ok) { - const projData = await projRes.json(); - setProjects(projData.projects ?? []); - } - // 구매한 팩 자료 파일 조회 const filesRes = await fetch('/api/packs/list-mine'); if (filesRes.ok) { @@ -174,38 +123,6 @@ export default function MyPage() { init(); }, []); - // ── 구독 해지 ── - const handleCancelSubscription = async (subId: string) => { - if (!confirm('구독을 해지하시겠습니까?\n만료일까지는 서비스를 계속 이용할 수 있습니다.')) return; - const res = await fetch(`/api/subscription/${subId}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ action: 'cancel' }), - }); - if (res.ok) { - setActiveSubscriptions((prev) => - prev.map((s) => s.id === subId ? { ...s, status: 'cancelled', auto_renew: false, cancelled_at: new Date().toISOString() } : s) - ); - } else { - alert('해지 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'); - } - }; - - // ── 자동갱신 토글 ── - const handleToggleAutoRenew = async (subId: string) => { - const res = await fetch(`/api/subscription/${subId}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ action: 'toggle_autorenew' }), - }); - if (res.ok) { - const data = await res.json(); - setActiveSubscriptions((prev) => - prev.map((s) => s.id === subId ? { ...s, auto_renew: data.auto_renew } : s) - ); - } - }; - // ── 텔레그램 연결 ── const handleTelegramConnect = async () => { setTelegramLinkState('generating'); @@ -271,844 +188,659 @@ export default function MyPage() { } } - const handleLinkProject = async (e: React.FormEvent) => { - e.preventDefault(); - if (!linkToken.trim()) return; - setLinking(true); - setLinkMessage(''); - try { - const res = await fetch('/api/projects/link', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ token: linkToken.trim() }), - }); - const data = await res.json(); - if (res.ok) { - setLinkMessage('프로젝트가 연결되었습니다!'); - setLinkToken(''); - const projRes = await fetch('/api/projects'); - if (projRes.ok) setProjects((await projRes.json()).projects ?? []); - } else { - setLinkMessage(data.error ?? '연결 중 오류가 발생했습니다.'); - } - } catch { - setLinkMessage('연결 중 오류가 발생했습니다.'); - } - setLinking(false); - }; - if (loading) { return ( -
-
+
+
); } if (!user) return null; - const activeSubs = activeSubscriptions.filter((s) => s.status === 'active' || s.status === 'cancelled'); - + // contact_requests 중 팩 주문만 추려 '내 제품' 탭에서 다운로드 노출 const packOrders = orders .map((o) => ({ order: o, tier: extractPackTier(o.service) })) .filter((x): x is { order: Order; tier: PackTier } => x.tier !== null); const tabs: { key: Tab; label: string; count?: number }[] = [ - { key: 'projects', label: '프로젝트 현황', count: projects.length || undefined }, - { key: 'orders', label: '의뢰 내역', count: orders.length || undefined }, - { key: 'payments', label: '결제 내역', count: payments.length || undefined }, - { key: 'packs', label: '구매한 팩', count: packOrders.length || undefined }, - { key: 'profile', label: '내 정보' }, - { key: 'subscription', label: '구독 관리', count: activeSubs.length || undefined }, - { key: 'saju', label: '사주 기록', count: sajuRecords.length || undefined }, + { key: 'profile', label: '프로필' }, + { key: 'requests', label: '내 의뢰', count: orders.length || undefined }, + { key: 'products', label: '내 제품', count: packOrders.length || undefined }, + { key: 'orders', label: '주문 내역', count: (orders.length + payments.length) || undefined }, ]; + function selectTab(key: Tab) { + setTab(key); + const params = new URLSearchParams(searchParams.toString()); + params.set('tab', key); + router.replace(`/mypage?${params.toString()}`, { scroll: false }); + } + return ( -
+
{/* 텔레그램 가이드 모달 */} {showTelegramGuide && ( setShowTelegramGuide(false)} /> )} - {/* 헤더 — kx-surface 다크 톤, 축소판. 로그아웃은 TopNav에서 담당 */} -
-
+ {/* ─── 페이지 헤더 ─── */} +
+
+ + 마이페이지 +
-
-
{user.email}
-
+
+
+ {user.email} +
+
가입일 {new Date(user.created_at).toLocaleDateString('ko-KR')}
+ + {/* ─── 탭 바 (상단 가로 탭 · 모바일 스크롤) ─── */} +
+
+ {tabs.map((t) => { + const active = tab === t.key; + return ( + + ); + })} +
+
-
- {/* 탭 */} -
- {tabs.map((t) => ( - - ))} -
- - {/* 탭 콘텐츠 */} - - {/* 내 정보 */} + {/* ─── 탭 콘텐츠 ─── */} +
+ {/* ===== 프로필 ===== */} {tab === 'profile' && ( -
-
-

-
- 계정 정보 -

-
-
- 이메일 - {user.email} -
-
- 로그인 방법 - - {user.app_metadata?.provider === 'google' ? 'Google' : '이메일'} - -
-
- 가입일 - - {new Date(user.created_at).toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric' })} - -
+
+ + 계정 정보 +
+ + +
-
- - {/* 구독 중인 서비스 - 요약 (탭으로 유도) */} - {activeSubs.length > 0 && ( -
-
- 🎟 -
-
- 서비스 구독 중 -
-
- {Math.max(0, Math.ceil((new Date(activeSubs[0].expires_at).getTime() - Date.now()) / 86400000))}일 후 만료 - {activeSubs[0].status === 'cancelled' && ' · 해지 예정'} -
-
-
- -
- )} + {/* 텔레그램 연동 카드 */} -
-

-
- 텔레그램 알림 연동 + +
+ 텔레그램 알림 연동 - 플래티넘 · 다이아 전용 -

- - {telegramChatId ? ( - /* ── 연결됨 ── */ -
-
-
- - - -
-
-
- 연결됨 - -
-
Chat ID: {telegramChatId}
-
-
- -
- ) : telegramLinkState === 'waiting' ? ( - /* ── 연결 대기 중 ── */ -
-
-

📱 아래 순서로 진행하세요

-
    -
  1. 아래 버튼을 클릭해 텔레그램 봇을 엽니다
  2. -
  3. 텔레그램에서 시작 버튼을 누릅니다
  4. -
  5. 봇이 "연결 완료" 메시지를 보내면 새로고침을 눌러주세요
  6. -
-

⏱ 유효시간: {telegramLinkExpiry}까지

-
-
- - - - - 텔레그램 봇 열기 - - - -
-
- ) : ( - /* ── 미연결 ── */ -
-
-
- - - -
-
-
연결 안 됨
-
텔레그램으로 번호를 바로 받아보세요
-
-
- -
- )} -
- -
-

-
- 빠른 메뉴 -

-
- -
- - - -
-
-
사주 분석
-
새 사주 보기
-
- - -
- - - -
-
-
외주 의뢰
-
프로젝트 문의
-
- - -
- - - -
-
-
AI 스튜디오
-
새 트랙 만들기
-
- + + 플래티넘 · 다이아 전용 +
-
-
- )} - {/* 구독 관리 */} - {tab === 'subscription' && ( -
- {activeSubscriptions.length === 0 ? ( - - ) : ( - activeSubscriptions.map((sub) => { - const expiresDate = new Date(sub.expires_at); - const daysLeft = Math.max(0, Math.ceil((expiresDate.getTime() - Date.now()) / 86400000)); - const isExpired = sub.status === 'expired'; - const isCancelled = sub.status === 'cancelled'; - const isActive = sub.status === 'active'; - - return ( -
- {/* 헤더 */} -
-
- 🎟 -
-
- {sub.product_id} -
-
- {new Date(sub.started_at).toLocaleDateString('ko-KR')} 시작 -
-
+
+ {telegramChatId ? ( + /* ── 연결됨 ── */ +
+
+
+
- - {isActive ? '이용 중' : isCancelled ? '해지 예정' : '만료됨'} - -
- - {/* 만료 정보 */} -
-
-
만료일
-
- {expiresDate.toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric' })} -
-
-
-
남은 기간
-
- {isExpired ? '만료됨' : `D-${daysLeft}`} -
-
-
- - {/* 자동갱신 토글 */} - {!isExpired && ( -
-
-
자동 갱신
-
- {sub.auto_renew ? '만료 시 자동으로 갱신됩니다' : '만료 시 자동 갱신되지 않습니다'} -
-
- -
- )} - - {/* 해지 취소 버튼 */} - {isCancelled && ( -
- 해지 신청됨 · {expiresDate.toLocaleDateString('ko-KR')}까지 서비스를 이용할 수 있습니다. - {sub.cancelled_at && ` (해지일: ${new Date(sub.cancelled_at).toLocaleDateString('ko-KR')})`} -
- )} - - {/* 액션 버튼 */} -
- - 외주 의뢰하기 - - {isActive && ( - - )} -
-
- ); - }) - )} - - {/* 서비스 이동 */} - -
- )} - - {/* 사주 기록 */} - {tab === 'saju' && ( -
- {sajuRecords.length === 0 ? ( - - ) : ( -
- {sajuRecords.map((rec) => ( -
-
-
{new Date(rec.created_at).toLocaleDateString('ko-KR')}
-
- {rec.saju_data?.birth_year ?? '?'}년{' '} - {rec.saju_data?.birth_month ?? '?'}월{' '} - {rec.saju_data?.birth_day ?? '?'}일생 +
+ 연결됨 +
-
- {rec.saju_data?.gender === 'male' ? '남성' : '여성'} - {rec.saju_data?.birth_hour != null ? ` · ${rec.saju_data.birth_hour}시생` : ''} +
+ Chat ID: {telegramChatId}
- - {rec.is_paid ? '유료' : '무료'} -
- {rec.interpretation && ( -

- {rec.interpretation.replace(/[#*]/g, '').substring(0, 80)}... + +

+ ) : telegramLinkState === 'waiting' ? ( + /* ── 연결 대기 중 ── */ +
+
+

+ 아래 순서로 진행하세요

- )} - - {rec.is_paid && rec.interpretation ? 'AI 해석 다시 보기 →' : '결과 보기 →'} - +
    +
  1. 아래 버튼을 클릭해 텔레그램 봇을 엽니다
  2. +
  3. 텔레그램에서 시작 버튼을 누릅니다
  4. +
  5. 봇이 "연결 완료" 메시지를 보내면 새로고침을 눌러주세요
  6. +
+

+ 유효시간: {telegramLinkExpiry}까지 +

+
+
+ + + 텔레그램 봇 열기 + + + +
- ))} -
- )} -
- )} - - {/* 결제 내역 */} - {tab === 'payments' && ( -
- {payments.length === 0 ? ( - - ) : ( -
- - - - - - - - - - - {payments.map((p, i) => ( - - - - - - - ))} - -
서비스금액상태일시
{p.product_name}₩{p.amount?.toLocaleString()} - - {p.status === 'paid' ? '결제완료' : p.status} - - - {new Date(p.created_at).toLocaleDateString('ko-KR')} -
-
- )} -
- )} - - {/* 구매한 팩 */} - {tab === 'packs' && ( -
- {packOrders.length === 0 ? ( - - ) : ( - packOrders.map(({ order, tier }) => { - const statusLabel = - order.status === 'completed' ? '자료 발송 완료' : - order.status === 'in_progress' ? '결제 처리 중' : - '입금 대기'; - const statusColor = - order.status === 'completed' ? 'bg-violet-50 text-violet-600 border-violet-200' : - order.status === 'in_progress' ? 'bg-amber-50 text-amber-600 border-amber-200' : - 'bg-slate-100 text-slate-500 border-slate-200'; - - return ( -
-
+ ) : ( + /* ── 미연결 ── */ +
+
+
+ +
-
{PACK_TIER_NAMES[tier]}
-
- {new Date(order.created_at).toLocaleDateString('ko-KR')} 신청 +
+ 연결 안 됨 +
+
+ 텔레그램으로 알림을 바로 받아보세요
- - {statusLabel} -
- - {/* 자료 리스트 — DB가 SSOT */} - {(() => { - const filesForTier = packFiles.filter((pf) => { - if (tier === 'starter') return pf.min_tier === 'starter'; - if (tier === 'pro') return pf.min_tier === 'starter' || pf.min_tier === 'pro'; - return true; // master - }); - - return ( -
-
- 📦 자료 패키지 ({filesForTier.length}개) -
- {filesForTier.length === 0 ? ( -

자료 준비 중. 카톡 1:1로 문의해주세요.

- ) : ( -
    - {filesForTier.map((f) => ( -
  • - {f.label} - {order.status === 'completed' ? ( - - ) : ( - 대기 중 - )} -
  • - ))} -
- )} - - {order.status === 'completed' && filesForTier.length > 0 && ( -

- ※ 다운로드 링크는 4시간 동안 유효합니다. -

- )} - - {order.status !== 'completed' && ( -

- {order.status === 'in_progress' ? '결제 처리 중. 자료는 결제 확인 후 활성화됩니다.' : '입금 대기 중. 카톡 1:1로 안내드립니다.'} -
- - 카톡 오픈채팅 → - -

- )} -
- ); - })()} +
- ); - }) - )} + )} +
+ + + {/* 빠른 메뉴 */} + + 빠른 메뉴 +
+ + + +
+
)} - {/* 프로젝트 진행 현황 */} - {tab === 'projects' && ( -
- {projects.length === 0 ? ( -
-
- - - -
-

진행 중인 프로젝트가 없습니다

-

외주 개발을 의뢰하시면 이곳에서 단계별 진행 현황을 실시간으로 확인할 수 있습니다.

- - 개발 의뢰하기 → - -
- ) : ( -
- {projects.map((project) => { - const totalSteps = project.milestones.length; - const completedSteps = project.milestones.filter((m) => m.status === 'completed').length; - const currentStep = project.milestones.find((m) => m.status === 'in_progress'); - const progressPct = totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0; - - return ( -
- {/* 헤더 */} -
-
-

{project.title}

-

- {project.total > 0 ? `총 ${project.total.toLocaleString()}원` : '금액 협의 중'} · {new Date(project.created_at).toLocaleDateString('ko-KR')} -

-
- - {project.status === 'sent' ? '견적 검토 중' : - project.status === 'accepted' ? '계약 완료' : - project.status === 'in_progress' ? '개발 진행 중' : - project.status === 'completed' ? '납품 완료' : project.status} - -
- -
- {/* 진행률 바 */} - {totalSteps > 0 && ( -
-
- 전체 진행률 - {progressPct}% ({completedSteps}/{totalSteps}단계) -
-
-
-
-
- )} - - {/* 현재 진행 단계 */} - {currentStep && ( -
-
- - 현재 진행 중 -
-

{currentStep.title}

- {currentStep.note && ( -

{currentStep.note}

- )} -
- )} - - {/* 단계별 타임라인 */} - {project.milestones.length > 0 && ( -
- {project.milestones.map((m, idx) => ( -
- {/* 아이콘 */} -
- {m.status === 'completed' ? ( - - - - ) : m.status === 'in_progress' ? ( - - - - - ) : m.step_number} -
- - {/* 수직 연결선 */} -
-
- {m.title} - {m.status === 'completed' && m.completed_at && ( - - {new Date(m.completed_at).toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' })} - - )} -
- {m.note && m.status !== 'pending' && ( -

{m.note}

- )} -
-
- ))} -
- )} -
-
- ); - })} -
- )} - - {/* 견적서 연결 폼 */} -
-

견적서 코드로 프로젝트 연결

-

견적서 링크를 받으셨나요? URL 끝의 코드를 입력하면 이 계정에서 진행 현황을 확인할 수 있습니다.

-
- setLinkToken(e.target.value)} - placeholder="예: abc123xyz" - className="flex-1 px-4 py-2 bg-white border border-slate-200 rounded-xl text-sm focus:outline-none focus:border-violet-400 min-w-0" - /> - -
- {linkMessage && ( -

- {linkMessage} -

- )} -
-
- )} - - {/* 의뢰 내역 */} - {tab === 'orders' && ( + {/* ===== 내 의뢰 ===== */} + {tab === 'requests' && (
{orders.length === 0 ? ( ) : (
{orders.map((o) => ( -
-
-
{o.service}
- - {o.status === 'completed' ? '완료' : o.status === 'in_progress' ? '진행중' : '대기중'} - + +
+
+ {o.service} +
+
-

{o.message}

-
{new Date(o.created_at).toLocaleDateString('ko-KR')}
-
+

+ {o.message} +

+
+ {new Date(o.created_at).toLocaleDateString('ko-KR')} +
+ ))}
)}
)} + + {/* ===== 내 제품 (구매한 팩) ===== */} + {tab === 'products' && ( +
+ {packOrders.length === 0 ? ( + + ) : ( + packOrders.map(({ order, tier }) => { + const completed = order.status === 'completed'; + const filesForTier = packFiles.filter((pf) => { + if (tier === 'starter') return pf.min_tier === 'starter'; + if (tier === 'pro') return pf.min_tier === 'starter' || pf.min_tier === 'pro'; + return true; // master + }); + + return ( + +
+
+
+ {PACK_TIER_NAMES[tier]} +
+
+ {new Date(order.created_at).toLocaleDateString('ko-KR')} 신청 +
+
+ +
+ +
+
+ 자료 패키지 ({filesForTier.length}개) +
+ + {filesForTier.length === 0 ? ( +

+ 자료 준비 중입니다. 카톡 1:1로 문의해주세요. +

+ ) : ( +
    + {filesForTier.map((f) => ( +
  • + + {f.label} + + {completed ? ( + + ) : ( + + 대기 중 + + )} +
  • + ))} +
+ )} + + {completed && filesForTier.length > 0 && ( +

+ 다운로드 링크는 발급 후 4시간 동안 유효합니다. +

+ )} + + {!completed && ( +
+ 입금 확인 후 다운로드가 활성화됩니다. + {order.status === 'in_progress' + ? ' 결제 처리 중입니다.' + : ' 입금 안내는 카톡 1:1로 드립니다.'} +
+ + 카톡 오픈채팅 → + +
+ )} +
+
+ ); + }) + )} +
+ )} + + {/* ===== 주문 내역 (의뢰 + 결제 완료) ===== */} + {tab === 'orders' && ( +
+ {/* 주문 목록 (contact_requests) */} +
+ 주문 목록 + {orders.length === 0 ? ( + + ) : ( +
+ {orders.map((o) => ( + +
+
+ {o.service} +
+ +
+

+ {o.message} +

+
+ {new Date(o.created_at).toLocaleDateString('ko-KR')} +
+
+ ))} +
+ )} +
+ + {/* 결제 완료 내역 (payments) */} +
+ 결제 완료 내역 + {payments.length === 0 ? ( +
+ 결제 완료된 내역이 아직 없습니다. +
+ ) : ( +
+ + + + + + + + + + + {payments.map((p) => ( + + + + + + + ))} + +
서비스금액상태일시
{p.product_name}₩{p.amount?.toLocaleString()} + + {p.status === 'paid' ? '결제완료' : p.status} + + + {new Date(p.created_at).toLocaleDateString('ko-KR')} +
+
+ )} +
+
+ )}
); } -function EmptyState({ - icon, title, desc, linkHref, linkLabel, +export default function MyPage() { + return ( + +
+
+ } + > + +
+ ); +} + +/* ─────────── 공통 프레젠테이션 컴포넌트 ─────────── */ + +function Card({ + children, + compact = false, }: { - icon: string; title: string; desc: string; linkHref: string; linkLabel: string; + children: React.ReactNode; + compact?: boolean; }) { return ( -
-
{icon}
-
{title}
-
{desc}
+
+ {children} +
+ ); +} + +function CardTitle({ children }: { children: React.ReactNode; inline?: boolean }) { + return ( +

+ {children} +

+ ); +} + +function SectionHeading({ children }: { children: React.ReactNode }) { + return ( +

+ {children} +

+ ); +} + +function Row({ label, value, last = false }: { label: string; value: string; last?: boolean }) { + return ( +
+ {label} + {value} +
+ ); +} + +function QuickLink({ href, title, sub }: { href: string; title: string; sub: string }) { + return ( + + {title} + {sub} + + ); +} + +// 상태 뱃지 — pending=surface-alt / in_progress=accent-soft / completed=성공 그린(예외 허용) +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)' }, + }; + return ( + + {conf.label} + + ); +} + +function EmptyState({ + title, + desc, + linkHref, + linkLabel, +}: { + title: string; + desc: string; + linkHref: string; + linkLabel: string; +}) { + return ( +
+
+ {title} +
+
+ {desc} +
{linkLabel} →
); } + +function TelegramIcon({ className, style }: { className?: string; style?: React.CSSProperties }) { + return ( + + + + ); +}