cherry2250 4df7ba0697 인증 영역 개발 완료 (로그인, 회원가입, 프로필 관리)
- 로그인 페이지: 이메일 + 비밀번호 로그인, 소셜 로그인 버튼
- 회원가입 페이지: 3단계 funnel (계정정보, 개인정보, 사업장정보)
- 프로필 관리 페이지: 기본정보/매장정보 수정, 비밀번호 변경, 로그아웃
- MUI v6 + React Hook Form + Zod 검증
- Next.js 14 App Router, TypeScript 5

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 11:27:15 +09:00

347 lines
9.9 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import {
Box,
TextField,
Button,
Checkbox,
FormControlLabel,
Typography,
Link,
Divider,
Paper,
InputAdornment,
IconButton,
} from '@mui/material';
import { Visibility, VisibilityOff, Email, Lock, ChatBubble } from '@mui/icons-material';
import { useAuthStore } from '@/stores/authStore';
import { useUIStore } from '@/stores/uiStore';
// 유효성 검사 스키마
const loginSchema = z.object({
email: z
.string()
.min(1, '이메일을 입력해주세요')
.email('올바른 이메일 형식이 아닙니다'),
password: z
.string()
.min(8, '비밀번호는 8자 이상이어야 합니다')
.regex(/^(?=.*[A-Za-z])(?=.*\d)/, '영문과 숫자를 포함해야 합니다'),
rememberMe: z.boolean().optional(),
});
type LoginFormData = z.infer<typeof loginSchema>;
export default function LoginPage() {
const router = useRouter();
const { login } = useAuthStore();
const { showToast, setLoading } = useUIStore();
const [showPassword, setShowPassword] = useState(false);
const {
control,
handleSubmit,
formState: { errors },
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
defaultValues: {
email: '',
password: '',
rememberMe: false,
},
});
const onSubmit = async (data: LoginFormData) => {
try {
setLoading(true);
// TODO: API 연동 시 실제 로그인 처리
// const response = await axios.post(`${USER_HOST}/api/v1/auth/login`, {
// email: data.email,
// password: data.password,
// });
// 임시 로그인 처리 (API 연동 전)
await new Promise(resolve => setTimeout(resolve, 1000));
const mockUser = {
id: '1',
name: '홍길동',
phone: '010-1234-5678',
email: data.email,
businessName: '홍길동 고깃집',
businessType: 'restaurant',
};
login(mockUser, 'mock-jwt-token');
showToast('로그인되었습니다', 'success');
router.push('/');
} catch {
showToast('로그인에 실패했습니다. 이메일과 비밀번호를 확인해주세요.', 'error');
} finally {
setLoading(false);
}
};
const handleSocialLogin = (provider: 'kakao' | 'naver') => {
// TODO: 소셜 로그인 구현
showToast(`${provider === 'kakao' ? '카카오톡' : '네이버'} 로그인은 준비 중입니다`, 'info');
};
return (
<Box
sx={{
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
px: 3,
py: 8,
background: 'linear-gradient(135deg, #FFF 0%, #F5F5F5 100%)',
}}
>
<Paper
elevation={0}
sx={{
width: '100%',
maxWidth: 440,
p: { xs: 4, sm: 6 },
borderRadius: 3,
boxShadow: '0 8px 32px rgba(0,0,0,0.08)',
}}
>
{/* 로고 및 타이틀 */}
<Box sx={{ textAlign: 'center', mb: 6 }}>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 64,
height: 64,
borderRadius: '50%',
bgcolor: 'primary.main',
mb: 3,
}}
>
<Typography sx={{ fontSize: 32 }}>🎉</Typography>
</Box>
<Typography variant="h4" sx={{ fontWeight: 700, mb: 1 }}>
KT AI
</Typography>
<Typography variant="body2" color="text.secondary">
</Typography>
</Box>
{/* 로그인 폼 */}
<form onSubmit={handleSubmit(onSubmit)}>
<Box sx={{ mb: 3 }}>
<Controller
name="email"
control={control}
render={({ field }) => (
<TextField
{...field}
fullWidth
label="이메일"
type="email"
placeholder="example@email.com"
error={!!errors.email}
helperText={errors.email?.message}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Email color="action" />
</InputAdornment>
),
}}
/>
)}
/>
</Box>
<Box sx={{ mb: 2 }}>
<Controller
name="password"
control={control}
render={({ field }) => (
<TextField
{...field}
fullWidth
type={showPassword ? 'text' : 'password'}
label="비밀번호"
placeholder="8자 이상 입력하세요"
error={!!errors.password}
helperText={errors.password?.message}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Lock color="action" />
</InputAdornment>
),
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={() => setShowPassword(!showPassword)}
edge="end"
size="small"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
)}
/>
</Box>
<Box sx={{ mb: 3 }}>
<Controller
name="rememberMe"
control={control}
render={({ field }) => (
<FormControlLabel
control={<Checkbox {...field} checked={field.value} />}
label={
<Typography variant="body2" color="text.secondary">
</Typography>
}
/>
)}
/>
</Box>
<Button
type="submit"
fullWidth
variant="contained"
size="large"
sx={{
mb: 2,
py: 1.5,
fontSize: 16,
fontWeight: 600,
}}
>
</Button>
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 2, mb: 4 }}>
<Link
href="/forgot-password"
variant="body2"
color="text.secondary"
underline="hover"
sx={{ cursor: 'pointer' }}
>
</Link>
<Typography variant="body2" color="text.secondary">
|
</Typography>
<Link
href="/register"
variant="body2"
color="primary"
underline="hover"
sx={{ cursor: 'pointer', fontWeight: 600 }}
>
</Link>
</Box>
</form>
{/* 소셜 로그인 */}
<Divider sx={{ mb: 3 }}>
<Typography variant="body2" color="text.secondary">
</Typography>
</Divider>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}>
<Button
fullWidth
variant="outlined"
size="large"
onClick={() => handleSocialLogin('kakao')}
sx={{
py: 1.5,
borderColor: '#FEE500',
bgcolor: '#FEE500',
color: '#000000',
fontWeight: 600,
'&:hover': {
bgcolor: '#FDD835',
borderColor: '#FDD835',
},
}}
startIcon={<ChatBubble />}
>
</Button>
<Button
fullWidth
variant="outlined"
size="large"
onClick={() => handleSocialLogin('naver')}
sx={{
py: 1.5,
borderColor: '#03C75A',
bgcolor: '#03C75A',
color: '#FFFFFF',
fontWeight: 600,
'&:hover': {
bgcolor: '#02B350',
borderColor: '#02B350',
},
}}
startIcon={
<Box
sx={{
width: 20,
height: 20,
borderRadius: '50%',
bgcolor: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 700,
color: '#03C75A',
fontSize: 14,
}}
>
N
</Box>
}
>
</Button>
</Box>
{/* 약관 동의 안내 */}
<Typography variant="caption" color="text.secondary" sx={{ textAlign: 'center', display: 'block' }}>
{' '}
<Link href="/terms" underline="hover" sx={{ color: 'text.secondary' }}>
</Link>{' '}
{' '}
<Link href="/privacy" underline="hover" sx={{ color: 'text.secondary' }}>
</Link>
.
</Typography>
</Paper>
</Box>
);
}