mirror of
https://github.com/ktds-dg0501/kt-event-marketing-fe.git
synced 2025-12-06 05:36:23 +00:00
- 로그인 페이지: 이메일 + 비밀번호 로그인, 소셜 로그인 버튼 - 회원가입 페이지: 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>
347 lines
9.9 KiB
TypeScript
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>
|
|
);
|
|
}
|