'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' | '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 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() { const router = useRouter(); const supabase = createClient(); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [tab, setTab] = useState('projects'); const [sajuRecords, setSajuRecords] = useState([]); 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 [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 ?? []); } 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: 'projects', label: '프로젝트 현황', count: projects.length || undefined }, { key: 'orders', label: '의뢰 내역', count: orders.length || undefined }, { key: 'payments', label: '결제 내역', count: payments.length || undefined }, { key: 'profile', label: '내 정보' }, { key: 'subscription', label: '구독 관리', count: activeSubs.length || undefined }, { key: 'saju', label: '사주 기록', count: sajuRecords.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 && (
🎟
서비스 구독 중
{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 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')} 시작
{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}시생` : ''}
{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} →
); }