'use client' // apps/web/src/app/accept-invite/page.tsx // 팀 초대 수락 페이지 — URL의 ?token=을 team-accept Edge Function으로 전달 import { Suspense, useEffect, useState } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import { Box, Stack, Alert, CircularProgress } from '@mui/material' import { MetalCard, PhosphorText } from '@d3ro/ui/components/ds' import { d3roPalette } from '@d3ro/ui/theme' import { getSupabaseBrowserClient, isSupabaseConfigured } from '@/lib/supabase-browser' type AcceptState = 'loading' | 'success' | 'error' | 'need_login' // useSearchParams는 Suspense boundary 필요 (Next.js 15 prerender 규칙) export default function AcceptInvitePage(): React.ReactElement { return ( } > ) } function AcceptInviteInner(): React.ReactElement { const router = useRouter() const searchParams = useSearchParams() const token = searchParams.get('token') const [state, setState] = useState('loading') const [message, setMessage] = useState('') const [teamId, setTeamId] = useState(null) useEffect(() => { async function run(): Promise { if (!token) { setState('error') setMessage('유효하지 않은 초대 링크입니다 (토큰 누락).') return } if (!isSupabaseConfigured()) { setState('error') setMessage('Supabase가 설정되지 않았습니다.') return } const supabase = getSupabaseBrowserClient() const { data: { session } } = await supabase.auth.getSession() if (!session) { setState('need_login') setMessage('초대를 수락하려면 먼저 로그인해주세요.') // 토큰을 sessionStorage에 저장해두고 로그인 후 돌아오도록 try { sessionStorage.setItem('pending_invite_token', token) } catch { // storage 차단 시 무시 } return } try { const response = await fetch( `${process.env.NEXT_PUBLIC_SUPABASE_URL}/functions/v1/team-accept`, { method: 'POST', headers: { Authorization: `Bearer ${session.access_token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ token }) } ) if (!response.ok) { const errData = (await response.json()) as { error?: string; message?: string } setState('error') setMessage(errData.message ?? errData.error ?? `실패: ${response.status}`) return } const data = (await response.json()) as { team_id: string; role: string } setTeamId(data.team_id) setState('success') setMessage(`팀에 가입되었습니다 (${data.role}). 잠시 후 이동합니다...`) // 성공 시 3초 후 팀 페이지로 setTimeout(() => { router.replace(`/teams/${data.team_id}`) }, 2000) } catch (e) { setState('error') setMessage(e instanceof Error ? e.message : 'Unknown error') } } void run() }, [token, router]) return ( TEAM INVITE {state === 'loading' && } {state === 'success' && ( {message} )} {state === 'error' && ( <> {message} 대시보드로 이동 )} {state === 'need_login' && ( <> {message} 로그인 페이지로 이동 )} {teamId && ( Team ID: {teamId} )} ) }