446 lines
16 KiB
TypeScript
446 lines
16 KiB
TypeScript
'use client'
|
|
|
|
// apps/web/src/app/login/page.tsx
|
|
// D3RO-VOICE Canonical Login Page — Precision Voice Intelligence Design
|
|
// Design Reference: docs/v3/designs/login.html
|
|
|
|
import { useRouter } from 'next/navigation'
|
|
import { useEffect, useState } from 'react'
|
|
import { d3roPalette } from '@d3ro/ui/theme'
|
|
import { Box, Stack, Alert } from '@mui/material'
|
|
import { useI18n } from '@d3ro/i18n'
|
|
import { getSupabaseBrowserClient, isSupabaseConfigured } from '@/lib/supabase-browser'
|
|
import { useAuth } from '@/components/providers/auth-provider'
|
|
|
|
export default function LoginPage(): React.ReactElement {
|
|
const router = useRouter()
|
|
const { user, loading } = useAuth()
|
|
const { t } = useI18n()
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [signingIn, setSigningIn] = useState(false)
|
|
const [email, setEmail] = useState('')
|
|
const [password, setPassword] = useState('')
|
|
const configured = isSupabaseConfigured()
|
|
|
|
useEffect(() => {
|
|
if (!loading && user) {
|
|
router.replace('/dashboard')
|
|
}
|
|
}, [loading, user, router])
|
|
|
|
async function handleOAuth(provider: 'google' | 'github' | 'apple'): Promise<void> {
|
|
if (!configured) {
|
|
setError('Supabase 환경변수가 설정되지 않았습니다.')
|
|
return
|
|
}
|
|
setError(null)
|
|
setSigningIn(true)
|
|
|
|
try {
|
|
const supabase = getSupabaseBrowserClient()
|
|
const redirectTo = `${window.location.origin}/auth/callback`
|
|
const { error: err } = await supabase.auth.signInWithOAuth({
|
|
provider: provider === 'apple' ? 'apple' : provider,
|
|
options: { redirectTo }
|
|
})
|
|
if (err) {
|
|
setError(err.message)
|
|
setSigningIn(false)
|
|
}
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Unknown error')
|
|
setSigningIn(false)
|
|
}
|
|
}
|
|
async function handleEmailSignIn(e: React.FormEvent): Promise<void> {
|
|
e.preventDefault()
|
|
if (!configured) {
|
|
setError('Supabase 환경변수가 설정되지 않았습니다.')
|
|
return
|
|
}
|
|
if (!email || !password) return
|
|
setError(null)
|
|
setSigningIn(true)
|
|
|
|
try {
|
|
const supabase = getSupabaseBrowserClient()
|
|
const { error: err } = await supabase.auth.signInWithPassword({ email, password })
|
|
if (err) {
|
|
setError(err.message)
|
|
setSigningIn(false)
|
|
} else {
|
|
router.replace('/dashboard')
|
|
}
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Unknown error')
|
|
setSigningIn(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Box
|
|
sx={{
|
|
minHeight: '100dvh',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
bgcolor: d3roPalette.bg.app,
|
|
p: 2,
|
|
fontFamily: '"Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Apple SD Gothic Neo", "Noto Sans KR", sans-serif'
|
|
}}
|
|
>
|
|
<Box
|
|
sx={{
|
|
width: '100%',
|
|
maxWidth: 400,
|
|
bgcolor: d3roPalette.bg.app,
|
|
border: '1px solid var(--d3-border-default)',
|
|
borderRadius: '24px',
|
|
p: { xs: 3.5, sm: 4.5 },
|
|
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.7)',
|
|
color: d3roPalette.text.secondary
|
|
}}
|
|
>
|
|
{/* Header & LED Indicators */}
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', mb: 4 }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25, mb: 2 }}>
|
|
<Box
|
|
sx={{
|
|
width: 10,
|
|
height: 10,
|
|
borderRadius: '50%',
|
|
bgcolor: d3roPalette.accent.main,
|
|
boxShadow: '0 0 10px var(--d3-accent-main)',
|
|
animation: 'pulse 2s infinite ease-in-out',
|
|
'@keyframes pulse': {
|
|
'0%, 100%': { opacity: 1, transform: 'scale(1)' },
|
|
'50%': { opacity: 0.6, transform: 'scale(0.92)' }
|
|
}
|
|
}}
|
|
/>
|
|
<Box
|
|
sx={{
|
|
width: 10,
|
|
height: 10,
|
|
borderRadius: '50%',
|
|
bgcolor: d3roPalette.tag.green,
|
|
boxShadow: '0 0 10px var(--d3-tag-green)'
|
|
}}
|
|
/>
|
|
</Box>
|
|
<Box
|
|
component="h1"
|
|
sx={{
|
|
m: 0,
|
|
fontSize: '22px',
|
|
fontWeight: 500,
|
|
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
letterSpacing: '0.25em',
|
|
color: d3roPalette.text.primary,
|
|
textIndent: '0.25em'
|
|
}}
|
|
>
|
|
D3RO-VOICE
|
|
</Box>
|
|
<Box
|
|
sx={{
|
|
mt: 1,
|
|
fontSize: '9px',
|
|
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
letterSpacing: '0.18em',
|
|
color: d3roPalette.text.label,
|
|
textTransform: 'uppercase'
|
|
}}
|
|
>
|
|
{t('login.subtitle') ?? 'Precision Voice Intelligence'}
|
|
</Box>
|
|
</Box>
|
|
|
|
{error && (
|
|
<Alert
|
|
severity="error"
|
|
variant="outlined"
|
|
sx={{
|
|
mb: 3,
|
|
bgcolor: 'color-mix(in srgb, var(--d3-tag-red) 8%, transparent)',
|
|
borderColor: 'color-mix(in srgb, var(--d3-tag-red) 30%, transparent)',
|
|
color: d3roPalette.tag.red,
|
|
fontSize: 12
|
|
}}
|
|
>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
{/* OAuth Buttons */}
|
|
<Stack spacing={1.5} sx={{ mb: 3 }}>
|
|
{/* Google Button */}
|
|
<Box
|
|
component="button"
|
|
type="button"
|
|
disabled={signingIn}
|
|
onClick={() => void handleOAuth('google')}
|
|
sx={{
|
|
width: '100%',
|
|
bgcolor: d3roPalette.bg.card,
|
|
border: '1px solid var(--d3-border-default)',
|
|
borderRadius: '12px',
|
|
py: 1.6,
|
|
px: 2,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: 1.5,
|
|
color: d3roPalette.text.primary,
|
|
fontSize: '14px',
|
|
fontWeight: 500,
|
|
cursor: signingIn ? 'not-allowed' : 'pointer',
|
|
transition: 'all 0.15s ease',
|
|
'&:hover': {
|
|
bgcolor: d3roPalette.bg.elevated,
|
|
borderColor: d3roPalette.border.strong
|
|
},
|
|
'&:active': {
|
|
transform: 'scale(0.99)'
|
|
}
|
|
}}
|
|
>
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M12.545,10.239v3.821h5.445c-0.712,2.315-2.647,3.972-5.445,3.972c-3.332,0-6.033-2.701-6.033-6.032s2.701-6.032,6.033-6.032c1.498,0,2.866,0.549,3.921,1.453l2.814-2.814C17.503,2.988,15.139,2,12.545,2C7.021,2,2.543,6.477,2.543,12s4.478,10,10.002,10c8.396,0,10.249-7.85,9.426-11.748L12.545,10.239z" />
|
|
</svg>
|
|
{t('login.google') ?? 'Google로 로그인'}
|
|
</Box>
|
|
|
|
{/* Apple Button */}
|
|
<Box
|
|
component="button"
|
|
type="button"
|
|
disabled={signingIn}
|
|
onClick={() => void handleOAuth('apple')}
|
|
sx={{
|
|
width: '100%',
|
|
bgcolor: d3roPalette.bg.card,
|
|
border: '1px solid var(--d3-border-default)',
|
|
borderRadius: '12px',
|
|
py: 1.6,
|
|
px: 2,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: 1.5,
|
|
color: d3roPalette.text.primary,
|
|
fontSize: '14px',
|
|
fontWeight: 500,
|
|
cursor: signingIn ? 'not-allowed' : 'pointer',
|
|
transition: 'all 0.15s ease',
|
|
'&:hover': {
|
|
bgcolor: d3roPalette.bg.elevated,
|
|
borderColor: d3roPalette.border.strong
|
|
},
|
|
'&:active': {
|
|
transform: 'scale(0.99)'
|
|
}
|
|
}}
|
|
>
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M15.4,8.3c-0.4-2.2,1.3-4.1,3.2-4.5c-0.5-2.2-2.3-3.6-4.5-3.6c-1.8-0.2-3.8,1.2-4.8,1.2c-1,0-2.6-1.1-4.1-1.1c-2,0-4,1.1-5,2.9C-1.8,7.2,0.6,13,2.6,15.9c1,1.4,2.2,3.1,3.8,3c1.5-0.1,2.1-1,3.9-1c1.8,0,2.3,1,3.9,1c1.6,0,2.6-1.5,3.6-2.9C18.9,14.4,19.3,13,19.3,12.9C19.2,12.8,15.9,11.5,15.4,8.3z M12.8,3.2c0.8-1,1.4-2.5,1.2-3.9C12.8,0.1,11.3,0.8,10.5,1.8C9.8,2.7,9.2,4.2,9.4,5.6C10.8,5.7,12,4.5,12.8,3.2z" />
|
|
</svg>
|
|
Apple로 로그인
|
|
</Box>
|
|
|
|
{/* GitHub Button */}
|
|
<Box
|
|
component="button"
|
|
type="button"
|
|
disabled={signingIn}
|
|
onClick={() => void handleOAuth('github')}
|
|
sx={{
|
|
width: '100%',
|
|
bgcolor: d3roPalette.bg.card,
|
|
border: '1px solid var(--d3-border-default)',
|
|
borderRadius: '12px',
|
|
py: 1.6,
|
|
px: 2,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: 1.5,
|
|
color: d3roPalette.text.primary,
|
|
fontSize: '14px',
|
|
fontWeight: 500,
|
|
cursor: signingIn ? 'not-allowed' : 'pointer',
|
|
transition: 'all 0.15s ease',
|
|
'&:hover': {
|
|
bgcolor: d3roPalette.bg.elevated,
|
|
borderColor: d3roPalette.border.strong
|
|
},
|
|
'&:active': {
|
|
transform: 'scale(0.99)'
|
|
}
|
|
}}
|
|
>
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
|
</svg>
|
|
{t('login.github') ?? 'GitHub로 로그인'}
|
|
</Box>
|
|
</Stack>
|
|
|
|
{/* Divider */}
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
|
<Box sx={{ flex: 1, height: '1px', bgcolor: d3roPalette.border.default }} />
|
|
<Box
|
|
sx={{
|
|
fontSize: '10px',
|
|
fontFamily: 'ui-monospace, monospace',
|
|
color: d3roPalette.text.label,
|
|
letterSpacing: '0.1em'
|
|
}}
|
|
>
|
|
{t('login.or') ?? '또는'}
|
|
</Box>
|
|
<Box sx={{ flex: 1, height: '1px', bgcolor: d3roPalette.border.default }} />
|
|
</Box>
|
|
|
|
{/* Email & Password Form */}
|
|
<Box component="form" onSubmit={handleEmailSignIn}>
|
|
<Stack spacing={2} sx={{ mb: 3 }}>
|
|
<Box>
|
|
<Box
|
|
component="label"
|
|
sx={{
|
|
display: 'block',
|
|
fontSize: '10px',
|
|
fontFamily: 'ui-monospace, monospace',
|
|
letterSpacing: '0.15em',
|
|
color: d3roPalette.text.label,
|
|
mb: 0.75,
|
|
pl: 0.5
|
|
}}
|
|
>
|
|
EMAIL
|
|
</Box>
|
|
<Box
|
|
component="input"
|
|
type="email"
|
|
required
|
|
placeholder="user@studio.com"
|
|
value={email}
|
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)}
|
|
sx={{
|
|
width: '100%',
|
|
bgcolor: d3roPalette.bg.app,
|
|
border: '1px solid var(--d3-border-default)',
|
|
borderRadius: '12px',
|
|
py: 1.6,
|
|
px: 2,
|
|
fontSize: '14px',
|
|
color: d3roPalette.text.primary,
|
|
outline: 'none',
|
|
boxSizing: 'border-box',
|
|
transition: 'border-color 0.15s ease',
|
|
'&:focus': {
|
|
borderColor: d3roPalette.accent.main
|
|
},
|
|
'&::placeholder': {
|
|
color: 'color-mix(in srgb, var(--d3-text-label) 40%, transparent)'
|
|
}
|
|
}}
|
|
/>
|
|
</Box>
|
|
|
|
<Box>
|
|
<Box
|
|
component="label"
|
|
sx={{
|
|
display: 'block',
|
|
fontSize: '10px',
|
|
fontFamily: 'ui-monospace, monospace',
|
|
letterSpacing: '0.15em',
|
|
color: d3roPalette.text.label,
|
|
mb: 0.75,
|
|
pl: 0.5
|
|
}}
|
|
>
|
|
PASSWORD
|
|
</Box>
|
|
<Box
|
|
component="input"
|
|
type="password"
|
|
required
|
|
placeholder="••••••••"
|
|
value={password}
|
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPassword(e.target.value)}
|
|
sx={{
|
|
width: '100%',
|
|
bgcolor: d3roPalette.bg.app,
|
|
border: '1px solid var(--d3-border-default)',
|
|
borderRadius: '12px',
|
|
py: 1.6,
|
|
px: 2,
|
|
fontSize: '14px',
|
|
color: d3roPalette.text.primary,
|
|
outline: 'none',
|
|
boxSizing: 'border-box',
|
|
transition: 'border-color 0.15s ease',
|
|
'&:focus': {
|
|
borderColor: d3roPalette.accent.main
|
|
},
|
|
'&::placeholder': {
|
|
color: 'color-mix(in srgb, var(--d3-text-label) 40%, transparent)'
|
|
}
|
|
}}
|
|
/>
|
|
</Box>
|
|
</Stack>
|
|
|
|
{/* Primary Orange CTA Button */}
|
|
<Box
|
|
component="button"
|
|
type="submit"
|
|
disabled={signingIn}
|
|
sx={{
|
|
width: '100%',
|
|
bgcolor: d3roPalette.accent.main,
|
|
border: 'none',
|
|
borderRadius: '12px',
|
|
py: 1.75,
|
|
fontSize: '14px',
|
|
fontWeight: 500,
|
|
letterSpacing: '0.05em',
|
|
color: d3roPalette.bg.app,
|
|
cursor: signingIn ? 'not-allowed' : 'pointer',
|
|
boxShadow: '0 0 25px rgba(59, 130, 246, 0.35)',
|
|
transition: 'all 0.15s ease',
|
|
'&:hover': {
|
|
filter: 'brightness(1.08)',
|
|
boxShadow: '0 0 30px rgba(59, 130, 246, 0.5)'
|
|
},
|
|
'&:active': {
|
|
transform: 'scale(0.99)'
|
|
}
|
|
}}
|
|
>
|
|
{signingIn ? '로그인 중...' : '로그인'}
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Footer */}
|
|
<Box sx={{ mt: 3.5, textAlign: 'center', fontSize: '11px', color: d3roPalette.text.label }}>
|
|
계정이 없으신가요?{' '}
|
|
<Box
|
|
component="span"
|
|
sx={{
|
|
color: d3roPalette.accent.main,
|
|
cursor: 'pointer',
|
|
fontWeight: 600,
|
|
'&:hover': { textDecoration: 'underline' }
|
|
}}
|
|
>
|
|
회원가입
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|