'use client'; 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'; import TelegramGuideModal from '@/app/components/TelegramGuideModal'; import { PACK_TIER_NAMES, extractPackTier, type PackTier } from '@/lib/pack-assets'; import type { PackFile } from '@/lib/supabase/pack-files'; import { KAKAO_OPENCHAT_URL } from '@/lib/contact'; // 마이페이지 — 4탭 재구성 (프로필 / 내 의뢰 / 내 제품 / 주문 내역). // PublicShell(TopNav)이 상단 내비·로그아웃을 제공하므로 여기서는 콘텐츠만 렌더한다. // 디자인은 메인(/)·외주(/outsourcing) 페이지의 --jsm-* 토큰·타이포 패턴과 일관되게 구성한다. 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'; // 구 탭 키 → 새 탭 키 매핑. 사주/구독/프로젝트 등 폐지 탭은 프로필로 폴백. 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 { id: string; created_at: string; amount: number; status: string; product_name: string; } interface Order { id: string; created_at: string; service: string; message: string; status: string; } 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(() => resolveTab(searchParams.get('tab'))); const [payments, setPayments] = useState([]); const [orders, setOrders] = useState([]); const [packFiles, setPackFiles] = useState([]); const [downloading, setDownloading] = useState(null); // 텔레그램 연동 상태 const [telegramChatId, setTelegramChatId] = useState(null); const [telegramLinkState, setTelegramLinkState] = useState('idle'); const [telegramDeepLink, setTelegramDeepLink] = useState(''); const [telegramLinkExpiry, setTelegramLinkExpiry] = useState(''); const [showTelegramGuide, setShowTelegramGuide] = useState(false); useEffect(() => { async function init() { const { data: { user } } = await supabase.auth.getUser(); if (!user) { router.push('/login'); return; } setUser(user); // 결제 내역 조회 const { data: pay } = await supabase .from('payments') .select('*') .eq('user_id', user.id) .order('created_at', { ascending: false }) .limit(20); setPayments(pay || []); // 의뢰 내역 조회 const { data: ord } = await supabase .from('contact_requests') .select('*') .eq('user_id', user.id) .order('created_at', { ascending: false }) .limit(20); setOrders(ord || []); // 텔레그램 chat_id 조회 const { data: profile } = await supabase .from('profiles') .select('telegram_chat_id') .eq('id', user.id) .maybeSingle(); setTelegramChatId(profile?.telegram_chat_id ?? null); // 구매한 팩 자료 파일 조회 const filesRes = await fetch('/api/packs/list-mine'); if (filesRes.ok) { const { files } = await filesRes.json(); setPackFiles(files ?? []); } setLoading(false); } init(); }, []); // ── 텔레그램 연결 ── const handleTelegramConnect = async () => { setTelegramLinkState('generating'); try { const res = await fetch('/api/telegram/connect', { method: 'POST' }); if (!res.ok) throw new Error('API_ERROR'); const data = await res.json(); setTelegramDeepLink(data.deepLink); setTelegramLinkExpiry(new Date(data.expiresAt).toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' })); setTelegramLinkState('waiting'); // 15분 후 자동으로 idle 복귀 setTimeout(() => setTelegramLinkState('idle'), 15 * 60 * 1000); } catch { setTelegramLinkState('idle'); alert('연결 코드 발급 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'); } }; // 연결 후 상태 새로고침 (버튼 클릭 시) const handleTelegramRefresh = async () => { const { data: profile } = await supabase .from('profiles') .select('telegram_chat_id') .eq('id', user!.id) .maybeSingle(); const chatId = profile?.telegram_chat_id ?? null; setTelegramChatId(chatId); if (chatId) setTelegramLinkState('idle'); }; // ── 텔레그램 연결 해제 ── const handleTelegramDisconnect = async () => { if (!confirm('텔레그램 연결을 해제하시겠습니까?')) return; setTelegramLinkState('disconnecting'); try { await fetch('/api/telegram/connect', { method: 'DELETE' }); setTelegramChatId(null); setTelegramDeepLink(''); } catch { alert('연결 해제 중 오류가 발생했습니다.'); } setTelegramLinkState('idle'); }; async function handleDownload(fileId: string) { setDownloading(fileId); try { const res = await fetch('/api/packs/sign-link', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileId }), }); const data = await res.json(); if (!res.ok || !data.url) { throw new Error(data.error ?? '링크 발급 실패'); } window.location.href = data.url; } catch (e) { alert(e instanceof Error ? e.message : '다운로드 준비 중 오류가 발생했습니다'); } finally { setDownloading(null); } } if (loading) { return (
); } if (!user) return null; // 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: '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)} /> )} {/* ─── 페이지 헤더 ─── */}
마이페이지
{user.email}
가입일 {new Date(user.created_at).toLocaleDateString('ko-KR')}
{/* ─── 탭 바 (상단 가로 탭 · 모바일 스크롤) ─── */}
{tabs.map((t) => { const active = tab === t.key; return ( ); })}
{/* ─── 탭 콘텐츠 ─── */}
{/* ===== 프로필 ===== */} {tab === 'profile' && (
계정 정보
{/* 텔레그램 연동 카드 */}
텔레그램 알림 연동 플래티넘 · 다이아 전용
{telegramChatId ? ( /* ── 연결됨 ── */
연결됨
Chat ID: {telegramChatId}
) : telegramLinkState === 'waiting' ? ( /* ── 연결 대기 중 ── */

아래 순서로 진행하세요

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

유효시간: {telegramLinkExpiry}까지

텔레그램 봇 열기
) : ( /* ── 미연결 ── */
연결 안 됨
텔레그램으로 알림을 바로 받아보세요
)}
{/* 빠른 메뉴 */} 빠른 메뉴
)} {/* ===== 내 의뢰 ===== */} {tab === 'requests' && (
{orders.length === 0 ? ( ) : (
{orders.map((o) => (
{o.service}

{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')}
)}
)}
); } export default function MyPage() { return (
} >
); } /* ─────────── 공통 프레젠테이션 컴포넌트 ─────────── */ function Card({ children, compact = false, }: { children: React.ReactNode; compact?: boolean; }) { return (
{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 ( ); }