'use client'; import { useEffect, useState } from 'react'; import { useRouter } 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'; 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 '/saju/input'; let url = `/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; } type Tab = 'profile' | 'projects' | 'subscription' | 'lotto' | 'saju' | 'payments' | '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; } 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; } interface LottoHistoryItem { id: number; numbers: number[]; source: string; plan_id: string; created_at: 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; } const PLAN_LABELS: Record = { lotto_gold: { label: 'κ³¨λ“œ', emoji: 'πŸ₯‡', color: 'amber' }, lotto_platinum: { label: 'ν”Œλž˜ν‹°λ„˜', emoji: 'πŸ’Ž', color: 'sky' }, lotto_diamond: { label: '닀이아', emoji: 'πŸ‘‘', color: 'violet' }, }; export default function MyPage() { const router = useRouter(); const supabase = createClient(); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [tab, setTab] = useState('profile'); const [sajuRecords, setSajuRecords] = useState([]); const [payments, setPayments] = useState([]); const [orders, setOrders] = useState([]); const [lottoHistory, setLottoHistory] = useState([]); const [activeSubscriptions, setActiveSubscriptions] = useState([]); const [projects, setProjects] = useState([]); const [linkToken, setLinkToken] = useState(''); const [linking, setLinking] = useState(false); const [linkMessage, setLinkMessage] = useState(''); // ν…”λ ˆκ·Έλž¨ 연동 μƒνƒœ 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: 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') .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); // ꡬ독 λͺ©λ‘ 쑰회 (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 { data: history } = await supabase .from('lotto_history') .select('id, numbers, source, plan_id, created_at') .eq('user_id', user.id) .order('created_at', { ascending: false }) .limit(50); setLottoHistory(history ?? []); setLoading(false); } init(); }, []); const handleLogout = async () => { await supabase.auth.signOut(); router.push('/'); router.refresh(); }; // ── ꡬ독 ν•΄μ§€ ── 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'); 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'); }; 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'); const tabs: { key: Tab; label: string; count?: number }[] = [ { key: 'profile', label: 'λ‚΄ 정보' }, { key: 'projects', label: 'ν”„λ‘œμ νŠΈ ν˜„ν™©', count: projects.length || undefined }, { key: 'subscription', label: 'ꡬ독 관리', count: activeSubs.length || undefined }, { key: 'lotto', label: '둜또 기둝', count: lottoHistory.length || undefined }, { key: 'saju', label: '사주 기둝', count: sajuRecords.length || undefined }, { key: 'payments', label: '결제 λ‚΄μ—­', count: payments.length || undefined }, { key: 'orders', label: '의뒰 λ‚΄μ—­', count: orders.length || undefined }, ]; return (
{/* ν…”λ ˆκ·Έλž¨ κ°€μ΄λ“œ λͺ¨λ‹¬ */} {showTelegramGuide && ( setShowTelegramGuide(false)} /> )} {/* 헀더 */}
{user.email?.[0].toUpperCase()}
{user.email}
κ°€μž…μΌ: {new Date(user.created_at).toLocaleDateString('ko-KR')}
{/* νƒ­ */}
{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 && (
{PLAN_LABELS[activeSubs[0].product_id]?.emoji ?? '🎟'}
둜또 {PLAN_LABELS[activeSubs[0].product_id]?.label} ꡬ독 쀑
{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. 봇이 "μ—°κ²° μ™„λ£Œ" λ©”μ‹œμ§€λ₯Ό 보내면 μƒˆλ‘œκ³ μΉ¨μ„ λˆŒλŸ¬μ£Όμ„Έμš”

⏱ μœ νš¨μ‹œκ°„: {telegramLinkExpiry}κΉŒμ§€

ν…”λ ˆκ·Έλž¨ 봇 μ—΄κΈ°
) : ( /* ── λ―Έμ—°κ²° ── */
μ—°κ²° μ•ˆ 됨
ν…”λ ˆκ·Έλž¨μœΌλ‘œ 번호λ₯Ό λ°”λ‘œ λ°›μ•„λ³΄μ„Έμš”
)}

λΉ λ₯Έ 메뉴

사주 뢄석
μƒˆ 사주 보기
둜또 번호 μΆ”μ²œ
κ΅¬λ…μž μ „μš©
μ™Έμ£Ό 의뒰
ν”„λ‘œμ νŠΈ 문의
)} {/* ꡬ독 관리 */} {tab === 'subscription' && (
{activeSubscriptions.length === 0 ? ( ) : ( activeSubscriptions.map((sub) => { const info = PLAN_LABELS[sub.product_id]; 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 (
{/* 헀더 */}
{info?.emoji ?? '🎟'}
둜또 번호 μΆ”μ²œ {info?.label ?? sub.product_id}
{new Date(sub.started_at).toLocaleDateString('ko-KR')} μ‹œμž‘
{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 === 'lotto' && (
{lottoHistory.length === 0 ? ( ) : (
총 {lottoHistory.length}개 μ‘°ν•© 생성
{lottoHistory.map((item) => { const info = PLAN_LABELS[item.plan_id]; return (
{item.numbers.map((n) => { const color = n <= 10 ? 'bg-yellow-400 text-yellow-900' : n <= 20 ? 'bg-blue-500 text-white' : n <= 30 ? 'bg-red-500 text-white' : n <= 40 ? 'bg-slate-500 text-white' : 'bg-green-500 text-white'; return ( {n} ); })}
{item.source === 'nas' ? 'NAS μΆ”μ²œ' : '둜컬 생성'} {info?.emoji} {info?.label} {new Date(item.created_at).toLocaleDateString('ko-KR', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
); })}
)}
)} {/* 사주 기둝 */} {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}μ‹œμƒ` : ''}
{rec.is_paid ? '유료' : '무료'}
{rec.interpretation && (

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

)} {rec.is_paid && rec.interpretation ? 'AI 해석 λ‹€μ‹œ 보기 β†’' : 'κ²°κ³Ό 보기 β†’'}
))}
)}
)} {/* 결제 λ‚΄μ—­ */} {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 === '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-[#dbe8ff] rounded-xl text-sm focus:outline-none focus:border-blue-400 min-w-0" />
{linkMessage && (

{linkMessage}

)}
)} {/* 의뒰 λ‚΄μ—­ */} {tab === 'orders' && (
{orders.length === 0 ? ( ) : (
{orders.map((o) => (
{o.service}
{o.status === 'completed' ? 'μ™„λ£Œ' : o.status === 'in_progress' ? '진행쀑' : 'λŒ€κΈ°μ€‘'}

{o.message}

{new Date(o.created_at).toLocaleDateString('ko-KR')}
))}
)}
)}
); } function EmptyState({ icon, title, desc, linkHref, linkLabel, }: { icon: string; title: string; desc: string; linkHref: string; linkLabel: string; }) { return (
{icon}
{title}
{desc}
{linkLabel} β†’
); }