feat: Complete Material-UI migration for all pages
- Updated Dashboard page to use Material-UI components - Updated SignupPage to use Material-UI with Google OAuth style - Fixed Material-UI icon import issues (replaced Activity with Timeline) - Updated CLAUDE.md to reflect UI framework migration from Lucide/Tailwind to Material-UI - All pages now follow consistent Google OAuth design pattern 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -25,7 +25,10 @@
|
|||||||
|
|
||||||
### 기술 스택
|
### 기술 스택
|
||||||
- **API Gateway**: Apache APISIX 3.8.0
|
- **API Gateway**: Apache APISIX 3.8.0
|
||||||
- **Frontend**: React 18 + Vite + TypeScript + shadcn/ui + Tailwind CSS
|
- **Frontend**: React 18 + Vite + TypeScript + Material-UI (MUI)
|
||||||
|
- 이전: Lucide React + Tailwind CSS
|
||||||
|
- 현재: Material-UI (@mui/material, @emotion/react, @emotion/styled, @mui/icons-material)
|
||||||
|
- 디자인: Google OAuth 스타일 UI/UX
|
||||||
- **Backend**: Python 3.11 + FastAPI + Motor (MongoDB async)
|
- **Backend**: Python 3.11 + FastAPI + Motor (MongoDB async)
|
||||||
- **Database**: MongoDB 7.0
|
- **Database**: MongoDB 7.0
|
||||||
- **Cache/Queue**: Redis 7
|
- **Cache/Queue**: Redis 7
|
||||||
|
|||||||
@ -1,31 +1,82 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
Box,
|
||||||
Users,
|
Container,
|
||||||
Settings,
|
Grid,
|
||||||
LogOut,
|
Card,
|
||||||
ChevronDown,
|
CardContent,
|
||||||
Bell,
|
Typography,
|
||||||
Search,
|
Button,
|
||||||
|
AppBar,
|
||||||
|
Toolbar,
|
||||||
|
IconButton,
|
||||||
|
Avatar,
|
||||||
Menu,
|
Menu,
|
||||||
X,
|
MenuItem,
|
||||||
Shield,
|
Drawer,
|
||||||
Key,
|
List,
|
||||||
Activity,
|
ListItem,
|
||||||
Clock
|
ListItemIcon,
|
||||||
} from 'lucide-react'
|
ListItemText,
|
||||||
|
ListItemButton,
|
||||||
|
Divider,
|
||||||
|
Paper,
|
||||||
|
LinearProgress,
|
||||||
|
Chip,
|
||||||
|
InputBase,
|
||||||
|
Badge
|
||||||
|
} from '@mui/material'
|
||||||
|
import {
|
||||||
|
Dashboard as DashboardIcon,
|
||||||
|
Apps,
|
||||||
|
Person,
|
||||||
|
AdminPanelSettings,
|
||||||
|
Menu as MenuIcon,
|
||||||
|
Logout,
|
||||||
|
Settings,
|
||||||
|
TrendingUp,
|
||||||
|
Security,
|
||||||
|
Speed,
|
||||||
|
AccessTime,
|
||||||
|
Notifications,
|
||||||
|
Search,
|
||||||
|
VpnKey,
|
||||||
|
Timeline
|
||||||
|
} from '@mui/icons-material'
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||||
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
||||||
const { user, logout } = useAuth()
|
const { user, logout } = useAuth()
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true)
|
const navigate = useNavigate()
|
||||||
const [isProfileOpen, setIsProfileOpen] = useState(false)
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await logout()
|
||||||
|
navigate('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleProfileMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setAnchorEl(event.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleProfileMenuClose = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ text: '대시보드', icon: <DashboardIcon />, path: '/dashboard' },
|
||||||
|
{ text: '애플리케이션', icon: <VpnKey />, path: '/applications' },
|
||||||
|
{ text: '프로필 설정', icon: <Settings />, path: '/profile' },
|
||||||
|
{ text: '관리자 패널', icon: <AdminPanelSettings />, path: '/admin', adminOnly: true },
|
||||||
|
]
|
||||||
|
|
||||||
const stats = [
|
const stats = [
|
||||||
{ title: '활성 세션', value: '24', icon: Users, change: '+12%' },
|
{ title: '활성 세션', value: '24', icon: <Person />, change: '+12%', color: '#1a73e8' },
|
||||||
{ title: '등록된 앱', value: '8', icon: Key, change: '+2' },
|
{ title: '등록된 앱', value: '8', icon: <VpnKey />, change: '+2', color: '#34a853' },
|
||||||
{ title: '이번 달 로그인', value: '1,429', icon: Activity, change: '+48%' },
|
{ title: '이번 달 로그인', value: '1,429', icon: <Timeline />, change: '+48%', color: '#fbbc04' },
|
||||||
{ title: '평균 응답시간', value: '132ms', icon: Clock, change: '-12%' },
|
{ title: '평균 응답시간', value: '132ms', icon: <AccessTime />, change: '-12%', color: '#ea4335' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const recentActivities = [
|
const recentActivities = [
|
||||||
@ -36,154 +87,209 @@ const Dashboard = () => {
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<Box sx={{ display: 'flex', minHeight: '100vh', backgroundColor: '#f8f9fa' }}>
|
||||||
{/* Sidebar */}
|
{/* AppBar */}
|
||||||
<aside className={`fixed top-0 left-0 z-40 h-screen transition-transform ${
|
<AppBar position="fixed" elevation={0} sx={{ backgroundColor: '#fff', borderBottom: '1px solid #dadce0' }}>
|
||||||
isSidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
<Toolbar>
|
||||||
} bg-white border-r border-gray-200 w-64`}>
|
<IconButton
|
||||||
<div className="flex items-center justify-between p-4 border-b">
|
edge="start"
|
||||||
<h1 className="text-xl font-bold text-gray-900">OAuth System</h1>
|
color="inherit"
|
||||||
<button onClick={() => setIsSidebarOpen(false)} className="lg:hidden">
|
aria-label="menu"
|
||||||
<X size={20} />
|
onClick={() => setDrawerOpen(true)}
|
||||||
</button>
|
sx={{ mr: 2, color: '#5f6368' }}
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav className="p-4 space-y-2">
|
|
||||||
<a href="/dashboard" className="flex items-center space-x-3 p-3 bg-blue-50 text-blue-600 rounded-lg">
|
|
||||||
<LayoutDashboard size={20} />
|
|
||||||
<span>대시보드</span>
|
|
||||||
</a>
|
|
||||||
<a href="/applications" className="flex items-center space-x-3 p-3 text-gray-700 hover:bg-gray-50 rounded-lg">
|
|
||||||
<Key size={20} />
|
|
||||||
<span>애플리케이션</span>
|
|
||||||
</a>
|
|
||||||
<a href="/profile" className="flex items-center space-x-3 p-3 text-gray-700 hover:bg-gray-50 rounded-lg">
|
|
||||||
<Settings size={20} />
|
|
||||||
<span>프로필 설정</span>
|
|
||||||
</a>
|
|
||||||
{user?.role === 'system_admin' && (
|
|
||||||
<a href="/admin" className="flex items-center space-x-3 p-3 text-gray-700 hover:bg-gray-50 rounded-lg">
|
|
||||||
<Shield size={20} />
|
|
||||||
<span>관리자 패널</span>
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div className="absolute bottom-4 left-4 right-4">
|
|
||||||
<button
|
|
||||||
onClick={logout}
|
|
||||||
className="flex items-center space-x-3 p-3 text-red-600 hover:bg-red-50 rounded-lg w-full"
|
|
||||||
>
|
>
|
||||||
<LogOut size={20} />
|
<MenuIcon />
|
||||||
<span>로그아웃</span>
|
</IconButton>
|
||||||
</button>
|
<Typography variant="h6" sx={{ flexGrow: 0, color: '#202124', fontWeight: 500, mr: 4 }}>
|
||||||
</div>
|
OAuth System
|
||||||
</aside>
|
</Typography>
|
||||||
|
|
||||||
|
{/* Search Bar */}
|
||||||
|
<Box sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
maxWidth: 600,
|
||||||
|
backgroundColor: '#f1f3f4',
|
||||||
|
borderRadius: 2,
|
||||||
|
px: 2,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}>
|
||||||
|
<Search sx={{ color: '#5f6368', mr: 1 }} />
|
||||||
|
<InputBase
|
||||||
|
placeholder="검색..."
|
||||||
|
sx={{ flex: 1, py: 1 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
|
|
||||||
|
<IconButton sx={{ mr: 2 }}>
|
||||||
|
<Badge badgeContent={3} color="error">
|
||||||
|
<Notifications sx={{ color: '#5f6368' }} />
|
||||||
|
</Badge>
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton onClick={handleProfileMenuOpen}>
|
||||||
|
<Avatar sx={{ width: 32, height: 32, bgcolor: '#1a73e8' }}>
|
||||||
|
{user?.name?.[0] || user?.email?.[0]?.toUpperCase() || 'U'}
|
||||||
|
</Avatar>
|
||||||
|
</IconButton>
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
onClose={handleProfileMenuClose}
|
||||||
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||||
|
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={() => { handleProfileMenuClose(); navigate('/profile'); }}>
|
||||||
|
<ListItemIcon><Person fontSize="small" /></ListItemIcon>
|
||||||
|
프로필 설정
|
||||||
|
</MenuItem>
|
||||||
|
<Divider />
|
||||||
|
<MenuItem onClick={handleLogout}>
|
||||||
|
<ListItemIcon><Logout fontSize="small" /></ListItemIcon>
|
||||||
|
로그아웃
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
|
||||||
|
{/* Drawer */}
|
||||||
|
<Drawer
|
||||||
|
anchor="left"
|
||||||
|
open={drawerOpen}
|
||||||
|
onClose={() => setDrawerOpen(false)}
|
||||||
|
>
|
||||||
|
<Box sx={{ width: 250 }}>
|
||||||
|
<Box sx={{ p: 2, borderBottom: '1px solid #dadce0' }}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 500, color: '#202124' }}>
|
||||||
|
OAuth System
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<List>
|
||||||
|
{menuItems.map((item) => {
|
||||||
|
if (item.adminOnly && user?.role !== 'system_admin') return null
|
||||||
|
return (
|
||||||
|
<ListItemButton
|
||||||
|
key={item.text}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(item.path)
|
||||||
|
setDrawerOpen(false)
|
||||||
|
}}
|
||||||
|
selected={location.pathname === item.path}
|
||||||
|
sx={{
|
||||||
|
'&.Mui-selected': {
|
||||||
|
backgroundColor: '#e8f0fe',
|
||||||
|
color: '#1967d2',
|
||||||
|
'& .MuiListItemIcon-root': {
|
||||||
|
color: '#1967d2',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon sx={{ color: '#5f6368' }}>{item.icon}</ListItemIcon>
|
||||||
|
<ListItemText primary={item.text} sx={{ color: 'inherit' }} />
|
||||||
|
</ListItemButton>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
<Divider />
|
||||||
|
<List>
|
||||||
|
<ListItemButton onClick={handleLogout}>
|
||||||
|
<ListItemIcon sx={{ color: '#ea4335' }}><Logout /></ListItemIcon>
|
||||||
|
<ListItemText primary="로그아웃" sx={{ color: '#ea4335' }} />
|
||||||
|
</ListItemButton>
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className={`${isSidebarOpen ? 'lg:ml-64' : ''}`}>
|
<Box component="main" sx={{ flexGrow: 1, mt: 8, p: 3 }}>
|
||||||
{/* Header */}
|
<Container maxWidth="xl">
|
||||||
<header className="bg-white border-b border-gray-200">
|
{/* Page Header */}
|
||||||
<div className="flex items-center justify-between p-4">
|
<Box sx={{ mb: 4 }}>
|
||||||
<div className="flex items-center space-x-4">
|
<Typography variant="h4" sx={{ mb: 1, fontWeight: 400, color: '#202124' }}>
|
||||||
<button onClick={() => setIsSidebarOpen(!isSidebarOpen)}>
|
대시보드
|
||||||
<Menu size={24} />
|
</Typography>
|
||||||
</button>
|
<Typography sx={{ color: '#5f6368' }}>
|
||||||
<div className="relative">
|
OAuth 시스템 현황을 한눈에 확인하세요
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" size={20} />
|
</Typography>
|
||||||
<input
|
</Box>
|
||||||
type="text"
|
|
||||||
placeholder="검색..."
|
|
||||||
className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<button className="relative p-2 text-gray-600 hover:text-gray-900">
|
|
||||||
<Bell size={20} />
|
|
||||||
<span className="absolute top-0 right-0 w-2 h-2 bg-red-500 rounded-full"></span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<button
|
|
||||||
onClick={() => setIsProfileOpen(!isProfileOpen)}
|
|
||||||
className="flex items-center space-x-2 p-2 rounded-lg hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center text-white">
|
|
||||||
{user?.name?.[0] || user?.email?.[0]?.toUpperCase()}
|
|
||||||
</div>
|
|
||||||
<span className="hidden md:block text-sm font-medium text-gray-700">
|
|
||||||
{user?.name || user?.email}
|
|
||||||
</span>
|
|
||||||
<ChevronDown size={16} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{isProfileOpen && (
|
|
||||||
<div className="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-lg shadow-lg">
|
|
||||||
<a href="/profile" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
|
|
||||||
프로필 설정
|
|
||||||
</a>
|
|
||||||
<hr className="border-gray-200" />
|
|
||||||
<button onClick={logout} className="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50">
|
|
||||||
로그아웃
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Dashboard Content */}
|
|
||||||
<main className="p-6">
|
|
||||||
<div className="mb-6">
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900">대시보드</h2>
|
|
||||||
<p className="text-gray-600">OAuth 시스템 현황을 한눈에 확인하세요</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats Grid */}
|
{/* Stats Grid */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
<Grid container spacing={3} sx={{ mb: 4 }}>
|
||||||
{stats.map((stat) => (
|
{stats.map((stat) => (
|
||||||
<div key={stat.title} className="bg-white p-6 rounded-lg shadow">
|
<Grid item xs={12} sm={6} md={3} key={stat.title}>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<Card elevation={0} sx={{ border: '1px solid #dadce0' }}>
|
||||||
<div className="p-2 bg-blue-50 rounded-lg">
|
<CardContent>
|
||||||
<stat.icon className="text-blue-600" size={24} />
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
||||||
</div>
|
<Box sx={{
|
||||||
<span className={`text-sm font-medium ${
|
p: 1,
|
||||||
stat.change.startsWith('+') ? 'text-green-600' : 'text-red-600'
|
borderRadius: 1,
|
||||||
}`}>
|
backgroundColor: `${stat.color}20`,
|
||||||
{stat.change}
|
color: stat.color
|
||||||
</span>
|
}}>
|
||||||
</div>
|
{stat.icon}
|
||||||
<h3 className="text-2xl font-bold text-gray-900">{stat.value}</h3>
|
</Box>
|
||||||
<p className="text-sm text-gray-600">{stat.title}</p>
|
<Chip
|
||||||
</div>
|
label={stat.change}
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: stat.change.startsWith('+') ? '#e6f4ea' : '#fce8e6',
|
||||||
|
color: stat.change.startsWith('+') ? '#1e8e3e' : '#d33b27',
|
||||||
|
fontWeight: 500
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="h4" sx={{ fontWeight: 500, color: '#202124', mb: 0.5 }}>
|
||||||
|
{stat.value}
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: '#5f6368', fontSize: '14px' }}>
|
||||||
|
{stat.title}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Grid>
|
||||||
|
|
||||||
{/* Recent Activities */}
|
{/* Recent Activity */}
|
||||||
<div className="bg-white rounded-lg shadow">
|
<Paper elevation={0} sx={{ border: '1px solid #dadce0', borderRadius: 2 }}>
|
||||||
<div className="p-6 border-b border-gray-200">
|
<Box sx={{ p: 3, borderBottom: '1px solid #dadce0' }}>
|
||||||
<h3 className="text-lg font-semibold text-gray-900">최근 활동</h3>
|
<Typography variant="h6" sx={{ fontWeight: 500, color: '#202124' }}>
|
||||||
</div>
|
최근 활동
|
||||||
<div className="p-6">
|
</Typography>
|
||||||
<div className="space-y-4">
|
</Box>
|
||||||
{recentActivities.map((activity) => (
|
<List>
|
||||||
<div key={activity.id} className="flex items-center justify-between py-3 border-b border-gray-100 last:border-0">
|
{recentActivities.map((activity, index) => (
|
||||||
<div>
|
<React.Fragment key={activity.id}>
|
||||||
<p className="text-sm font-medium text-gray-900">{activity.action}</p>
|
<ListItem sx={{ py: 2.5 }}>
|
||||||
<p className="text-sm text-gray-600">{activity.app}</p>
|
<ListItemText
|
||||||
</div>
|
primary={
|
||||||
<span className="text-sm text-gray-500">{activity.time}</span>
|
<Typography sx={{ fontWeight: 500, color: '#202124' }}>
|
||||||
</div>
|
{activity.action}
|
||||||
))}
|
</Typography>
|
||||||
</div>
|
}
|
||||||
</div>
|
secondary={
|
||||||
</div>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 0.5 }}>
|
||||||
</main>
|
<Typography variant="body2" sx={{ color: '#5f6368' }}>
|
||||||
</div>
|
{activity.app}
|
||||||
</div>
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ color: '#5f6368' }}>
|
||||||
|
• {activity.time}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
{index < recentActivities.length - 1 && <Divider />}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,30 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
Eye,
|
Box,
|
||||||
EyeOff,
|
Container,
|
||||||
Loader2,
|
TextField,
|
||||||
Shield,
|
Button,
|
||||||
|
Typography,
|
||||||
|
Link,
|
||||||
|
Paper,
|
||||||
|
IconButton,
|
||||||
|
InputAdornment,
|
||||||
|
Checkbox,
|
||||||
|
FormControlLabel,
|
||||||
|
LinearProgress,
|
||||||
|
Alert,
|
||||||
|
Stack
|
||||||
|
} from '@mui/material'
|
||||||
|
import {
|
||||||
|
Visibility,
|
||||||
|
VisibilityOff,
|
||||||
|
Person,
|
||||||
|
Email,
|
||||||
Lock,
|
Lock,
|
||||||
Mail,
|
Business,
|
||||||
ArrowRight,
|
ArrowForward
|
||||||
User,
|
} from '@mui/icons-material'
|
||||||
Building,
|
|
||||||
Check
|
|
||||||
} from 'lucide-react'
|
|
||||||
|
|
||||||
const SignupPage = () => {
|
const SignupPage = () => {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
@ -25,7 +38,6 @@ const SignupPage = () => {
|
|||||||
const [showPassword, setShowPassword] = useState(false)
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [focusedInput, setFocusedInput] = useState<string | null>(null)
|
|
||||||
const [passwordStrength, setPasswordStrength] = useState(0)
|
const [passwordStrength, setPasswordStrength] = useState(0)
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -89,12 +101,12 @@ const SignupPage = () => {
|
|||||||
|
|
||||||
const getPasswordStrengthColor = () => {
|
const getPasswordStrengthColor = () => {
|
||||||
switch(passwordStrength) {
|
switch(passwordStrength) {
|
||||||
case 0: return 'bg-gray-400'
|
case 0: return '#bdbdbd'
|
||||||
case 1: return 'bg-red-500'
|
case 1: return '#f44336'
|
||||||
case 2: return 'bg-yellow-500'
|
case 2: return '#ff9800'
|
||||||
case 3: return 'bg-blue-500'
|
case 3: return '#2196f3'
|
||||||
case 4: return 'bg-green-500'
|
case 4: return '#4caf50'
|
||||||
default: return 'bg-gray-400'
|
default: return '#bdbdbd'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,293 +122,293 @@ const SignupPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex relative overflow-hidden bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900">
|
<Box
|
||||||
{/* Animated background elements */}
|
sx={{
|
||||||
<div className="absolute inset-0">
|
minHeight: '100vh',
|
||||||
<div className="absolute top-20 left-20 w-72 h-72 bg-purple-600 rounded-full mix-blend-multiply filter blur-xl opacity-30 animate-float"></div>
|
display: 'flex',
|
||||||
<div className="absolute top-40 right-20 w-72 h-72 bg-pink-600 rounded-full mix-blend-multiply filter blur-xl opacity-30 animate-float" style={{ animationDelay: '2s' }}></div>
|
flexDirection: 'column',
|
||||||
<div className="absolute bottom-20 left-1/2 w-72 h-72 bg-blue-600 rounded-full mix-blend-multiply filter blur-xl opacity-30 animate-float" style={{ animationDelay: '4s' }}></div>
|
backgroundColor: '#fff'
|
||||||
</div>
|
}}
|
||||||
|
>
|
||||||
|
<Container maxWidth="sm">
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
py: 4
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Logo and Title */}
|
||||||
|
<Box sx={{ textAlign: 'center', mb: 4 }}>
|
||||||
|
<Typography
|
||||||
|
variant="h4"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 300,
|
||||||
|
color: '#202124',
|
||||||
|
mb: 2,
|
||||||
|
fontFamily: 'Google Sans, Roboto, Arial, sans-serif'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box component="span" sx={{ fontWeight: 500 }}>AiMond</Box>
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h5"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 400,
|
||||||
|
color: '#202124',
|
||||||
|
mb: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
AiMond 계정 만들기
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: '#5f6368', fontSize: '16px' }}>
|
||||||
|
계속하려면 AiMond로 이동
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Grid pattern overlay */}
|
{/* Signup Form */}
|
||||||
<div className="absolute inset-0 opacity-20"
|
<Paper
|
||||||
style={{
|
elevation={0}
|
||||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%239C92AC' fill-opacity='0.05'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`
|
sx={{
|
||||||
}}></div>
|
border: '1px solid #dadce0',
|
||||||
|
borderRadius: '8px',
|
||||||
{/* Main content */}
|
p: 5,
|
||||||
<div className="relative z-10 w-full flex items-center justify-center p-4">
|
maxWidth: 450,
|
||||||
<div className="w-full max-w-5xl grid lg:grid-cols-2 gap-12 items-center">
|
width: '100%',
|
||||||
|
mx: 'auto'
|
||||||
{/* Left side - Branding */}
|
}}
|
||||||
<div className="hidden lg:block text-white space-y-8">
|
>
|
||||||
<div className="space-y-4">
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="flex items-center space-x-3">
|
<Stack spacing={2}>
|
||||||
<div className="p-3 bg-white/10 backdrop-blur-xl rounded-2xl">
|
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||||
<Shield className="w-8 h-8 text-white" />
|
<TextField
|
||||||
</div>
|
name="name"
|
||||||
<h1 className="text-4xl font-bold">
|
value={formData.name}
|
||||||
AiMond Authorization
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<p className="text-xl text-gray-300">
|
|
||||||
새로운 계정을 생성하고 시작하세요
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex items-start space-x-4">
|
|
||||||
<div className="p-2 bg-green-500/20 rounded-lg flex-shrink-0">
|
|
||||||
<Check className="w-5 h-5 text-green-400" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-lg mb-1">빠른 가입 절차</h3>
|
|
||||||
<p className="text-gray-400 text-sm">몇 가지 정보만으로 간단하게 가입 완료</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start space-x-4">
|
|
||||||
<div className="p-2 bg-blue-500/20 rounded-lg flex-shrink-0">
|
|
||||||
<Shield className="w-5 h-5 text-blue-400" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-lg mb-1">엔터프라이즈 보안</h3>
|
|
||||||
<p className="text-gray-400 text-sm">최고 수준의 보안으로 데이터 보호</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start space-x-4">
|
|
||||||
<div className="p-2 bg-purple-500/20 rounded-lg flex-shrink-0">
|
|
||||||
<Building className="w-5 h-5 text-purple-400" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-lg mb-1">조직 단위 관리</h3>
|
|
||||||
<p className="text-gray-400 text-sm">팀과 조직을 효율적으로 관리</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right side - Signup Form */}
|
|
||||||
<div className="w-full max-w-md mx-auto">
|
|
||||||
<div className="bg-white/10 backdrop-blur-2xl rounded-3xl p-8 shadow-2xl border border-white/20">
|
|
||||||
{/* Form Header */}
|
|
||||||
<div className="text-center mb-8">
|
|
||||||
<h2 className="text-3xl font-bold text-white mb-2">Create Account</h2>
|
|
||||||
<p className="text-gray-300">새로운 계정을 생성하세요</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Signup Form */}
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
|
||||||
{/* Name Input */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="name" className="text-sm font-medium text-gray-300 flex items-center space-x-2">
|
|
||||||
<User size={16} />
|
|
||||||
<span>이름</span>
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={formData.name}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
onFocus={() => setFocusedInput('name')}
|
|
||||||
onBlur={() => setFocusedInput(null)}
|
|
||||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-gray-400 focus:outline-none focus:border-purple-400 focus:bg-white/10 transition-all duration-300"
|
|
||||||
placeholder="홍길동"
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
<div className={`absolute bottom-0 left-0 h-0.5 bg-gradient-to-r from-purple-400 to-pink-400 transition-all duration-300 ${
|
|
||||||
focusedInput === 'name' ? 'w-full' : 'w-0'
|
|
||||||
}`}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Email Input */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="email" className="text-sm font-medium text-gray-300 flex items-center space-x-2">
|
|
||||||
<Mail size={16} />
|
|
||||||
<span>이메일 주소</span>
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
value={formData.email}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
onFocus={() => setFocusedInput('email')}
|
|
||||||
onBlur={() => setFocusedInput(null)}
|
|
||||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-gray-400 focus:outline-none focus:border-purple-400 focus:bg-white/10 transition-all duration-300"
|
|
||||||
placeholder="your@email.com"
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
<div className={`absolute bottom-0 left-0 h-0.5 bg-gradient-to-r from-purple-400 to-pink-400 transition-all duration-300 ${
|
|
||||||
focusedInput === 'email' ? 'w-full' : 'w-0'
|
|
||||||
}`}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Organization Input */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="organization" className="text-sm font-medium text-gray-300 flex items-center space-x-2">
|
|
||||||
<Building size={16} />
|
|
||||||
<span>조직/회사 (선택)</span>
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
id="organization"
|
|
||||||
name="organization"
|
|
||||||
type="text"
|
|
||||||
value={formData.organization}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
onFocus={() => setFocusedInput('organization')}
|
|
||||||
onBlur={() => setFocusedInput(null)}
|
|
||||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-gray-400 focus:outline-none focus:border-purple-400 focus:bg-white/10 transition-all duration-300"
|
|
||||||
placeholder="AiMond Inc."
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
<div className={`absolute bottom-0 left-0 h-0.5 bg-gradient-to-r from-purple-400 to-pink-400 transition-all duration-300 ${
|
|
||||||
focusedInput === 'organization' ? 'w-full' : 'w-0'
|
|
||||||
}`}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Password Input */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="password" className="text-sm font-medium text-gray-300 flex items-center space-x-2">
|
|
||||||
<Lock size={16} />
|
|
||||||
<span>비밀번호</span>
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type={showPassword ? 'text' : 'password'}
|
|
||||||
required
|
|
||||||
value={formData.password}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
onFocus={() => setFocusedInput('password')}
|
|
||||||
onBlur={() => setFocusedInput(null)}
|
|
||||||
className="w-full px-4 py-3 pr-12 bg-white/5 border border-white/10 rounded-xl text-white placeholder-gray-400 focus:outline-none focus:border-purple-400 focus:bg-white/10 transition-all duration-300"
|
|
||||||
placeholder="••••••••"
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
{showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
|
|
||||||
</button>
|
|
||||||
<div className={`absolute bottom-0 left-0 h-0.5 bg-gradient-to-r from-purple-400 to-pink-400 transition-all duration-300 ${
|
|
||||||
focusedInput === 'password' ? 'w-full' : 'w-0'
|
|
||||||
}`}></div>
|
|
||||||
</div>
|
|
||||||
{formData.password && (
|
|
||||||
<div className="flex items-center space-x-2 mt-2">
|
|
||||||
<div className="flex-1 bg-gray-700 rounded-full h-2">
|
|
||||||
<div className={`h-full rounded-full transition-all duration-300 ${getPasswordStrengthColor()}`}
|
|
||||||
style={{ width: `${passwordStrength * 25}%` }}></div>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-gray-400">{getPasswordStrengthText()}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Confirm Password Input */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="confirmPassword" className="text-sm font-medium text-gray-300 flex items-center space-x-2">
|
|
||||||
<Lock size={16} />
|
|
||||||
<span>비밀번호 확인</span>
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
id="confirmPassword"
|
|
||||||
name="confirmPassword"
|
|
||||||
type={showConfirmPassword ? 'text' : 'password'}
|
|
||||||
required
|
|
||||||
value={formData.confirmPassword}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
onFocus={() => setFocusedInput('confirmPassword')}
|
|
||||||
onBlur={() => setFocusedInput(null)}
|
|
||||||
className="w-full px-4 py-3 pr-12 bg-white/5 border border-white/10 rounded-xl text-white placeholder-gray-400 focus:outline-none focus:border-purple-400 focus:bg-white/10 transition-all duration-300"
|
|
||||||
placeholder="••••••••"
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
{showConfirmPassword ? <EyeOff size={20} /> : <Eye size={20} />}
|
|
||||||
</button>
|
|
||||||
<div className={`absolute bottom-0 left-0 h-0.5 bg-gradient-to-r from-purple-400 to-pink-400 transition-all duration-300 ${
|
|
||||||
focusedInput === 'confirmPassword' ? 'w-full' : 'w-0'
|
|
||||||
}`}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Terms Agreement */}
|
|
||||||
<div className="flex items-start space-x-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="agreeTerms"
|
|
||||||
name="agreeTerms"
|
|
||||||
checked={formData.agreeTerms}
|
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className="mt-1 w-4 h-4 bg-white/10 border-white/20 rounded text-purple-500 focus:ring-purple-500 focus:ring-offset-0"
|
placeholder="이름"
|
||||||
required
|
required
|
||||||
|
fullWidth
|
||||||
|
size="medium"
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<Person sx={{ color: '#5f6368' }} />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="agreeTerms" className="text-sm text-gray-300">
|
</Box>
|
||||||
<a href="#" className="text-purple-400 hover:text-purple-300">서비스 약관</a> 및{' '}
|
|
||||||
<a href="#" className="text-purple-400 hover:text-purple-300">개인정보 처리방침</a>에 동의합니다
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Submit Button */}
|
<TextField
|
||||||
<button
|
name="email"
|
||||||
type="submit"
|
type="email"
|
||||||
disabled={isLoading || !formData.agreeTerms}
|
value={formData.email}
|
||||||
className="w-full relative group overflow-hidden rounded-xl bg-gradient-to-r from-purple-500 to-pink-500 p-[2px] transition-all duration-300 hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
|
onChange={handleInputChange}
|
||||||
>
|
placeholder="이메일 주소"
|
||||||
<div className="relative flex items-center justify-center w-full bg-slate-900 back rounded-[10px] py-3 transition-all duration-300 group-hover:bg-opacity-0">
|
required
|
||||||
{isLoading ? (
|
fullWidth
|
||||||
<>
|
size="medium"
|
||||||
<Loader2 className="animate-spin mr-2" size={20} />
|
InputProps={{
|
||||||
<span className="font-semibold text-white">계정 생성 중...</span>
|
startAdornment: (
|
||||||
</>
|
<InputAdornment position="start">
|
||||||
) : (
|
<Email sx={{ color: '#5f6368' }} />
|
||||||
<>
|
</InputAdornment>
|
||||||
<span className="font-semibold text-white">계정 생성</span>
|
),
|
||||||
<ArrowRight className="ml-2 group-hover:translate-x-1 transition-transform" size={20} />
|
}}
|
||||||
</>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* Sign in link */}
|
<TextField
|
||||||
<p className="mt-6 text-center text-sm text-gray-400">
|
name="organization"
|
||||||
이미 계정이 있으신가요?{' '}
|
value={formData.organization}
|
||||||
<a href="/login" className="font-medium text-purple-400 hover:text-purple-300 transition-colors">
|
onChange={handleInputChange}
|
||||||
로그인하기
|
placeholder="조직/회사 (선택)"
|
||||||
</a>
|
fullWidth
|
||||||
</p>
|
size="medium"
|
||||||
</div>
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<Business sx={{ color: '#5f6368' }} />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Security Badge */}
|
<TextField
|
||||||
<div className="mt-6 flex items-center justify-center space-x-2 text-xs text-gray-400">
|
name="password"
|
||||||
<Lock size={12} />
|
type={showPassword ? 'text' : 'password'}
|
||||||
<span>256-bit SSL 암호화로 보호됨</span>
|
value={formData.password}
|
||||||
</div>
|
onChange={handleInputChange}
|
||||||
</div>
|
placeholder="비밀번호"
|
||||||
</div>
|
required
|
||||||
</div>
|
fullWidth
|
||||||
</div>
|
size="medium"
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<Lock sx={{ color: '#5f6368' }} />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
edge="end"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{formData.password && (
|
||||||
|
<Box>
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={passwordStrength * 25}
|
||||||
|
sx={{
|
||||||
|
height: 6,
|
||||||
|
borderRadius: 3,
|
||||||
|
backgroundColor: '#e0e0e0',
|
||||||
|
'& .MuiLinearProgress-bar': {
|
||||||
|
backgroundColor: getPasswordStrengthColor(),
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography variant="caption" sx={{ color: '#5f6368', mt: 0.5 }}>
|
||||||
|
비밀번호 강도: {getPasswordStrengthText()}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
name="confirmPassword"
|
||||||
|
type={showConfirmPassword ? 'text' : 'password'}
|
||||||
|
value={formData.confirmPassword}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="비밀번호 확인"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
size="medium"
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<Lock sx={{ color: '#5f6368' }} />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||||
|
edge="end"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography variant="caption" sx={{ color: '#5f6368', lineHeight: 1.5 }}>
|
||||||
|
문자, 숫자, 기호를 조합하여 8자 이상의 비밀번호를 사용하세요
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="agreeTerms"
|
||||||
|
checked={formData.agreeTerms}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
size="small"
|
||||||
|
sx={{ color: '#5f6368' }}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<Typography sx={{ fontSize: '14px', color: '#5f6368' }}>
|
||||||
|
<Link href="#" sx={{ color: '#1a73e8' }}>서비스 약관</Link> 및{' '}
|
||||||
|
<Link href="#" sx={{ color: '#1a73e8' }}>개인정보처리방침</Link>에 동의합니다
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 3 }}>
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
underline="none"
|
||||||
|
sx={{
|
||||||
|
color: '#1a73e8',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 500
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
대신 로그인
|
||||||
|
</Link>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
disabled={isLoading || !formData.agreeTerms}
|
||||||
|
endIcon={<ArrowForward />}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: '#1a73e8',
|
||||||
|
color: 'white',
|
||||||
|
textTransform: 'none',
|
||||||
|
px: 3,
|
||||||
|
py: 1,
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 500,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#1666c9',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isLoading ? '계정 생성 중...' : '다음'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Footer Text */}
|
||||||
|
<Box sx={{ mt: 4, textAlign: 'center' }}>
|
||||||
|
<Typography sx={{ fontSize: '12px', color: '#5f6368' }}>
|
||||||
|
하나의 AiMond 계정으로 모든 AiMond 서비스를 이용하실 수 있습니다
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
borderTop: '1px solid #dadce0',
|
||||||
|
py: 2,
|
||||||
|
px: 3,
|
||||||
|
backgroundColor: '#f8f9fa'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container maxWidth="lg">
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Typography sx={{ fontSize: '12px', color: '#5f6368' }}>한국어</Typography>
|
||||||
|
<Stack direction="row" spacing={3}>
|
||||||
|
<Link href="#" underline="none" sx={{ fontSize: '12px', color: '#5f6368' }}>
|
||||||
|
도움말
|
||||||
|
</Link>
|
||||||
|
<Link href="#" underline="none" sx={{ fontSize: '12px', color: '#5f6368' }}>
|
||||||
|
개인정보처리방침
|
||||||
|
</Link>
|
||||||
|
<Link href="#" underline="none" sx={{ fontSize: '12px', color: '#5f6368' }}>
|
||||||
|
약관
|
||||||
|
</Link>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user