eaf067db1c
Add AppPopup shell, shared CSS, MUI theme overrides, and center positioning for consistent modal styling across frontend and frontend_golden. Co-authored-by: Cursor <cursoragent@cursor.com>
224 lines
6.4 KiB
TypeScript
224 lines
6.4 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogActions,
|
|
TextField,
|
|
Button,
|
|
Box,
|
|
Typography,
|
|
Alert,
|
|
Link,
|
|
IconButton,
|
|
InputAdornment,
|
|
Divider,
|
|
} from '@mui/material';
|
|
import { Visibility, VisibilityOff, AccountCircle } from '@mui/icons-material';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
import DraggablePaper from './DraggablePaper';
|
|
import GoldenPopupDialogTitle from './GoldenPopupDialogTitle';
|
|
|
|
interface LoginDialogProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSwitchToSignUp: () => void;
|
|
}
|
|
|
|
const LoginDialog: React.FC<LoginDialogProps> = ({ open, onClose, onSwitchToSignUp }) => {
|
|
const { login } = useAuth();
|
|
const [usernameOrEmail, setUsernameOrEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
|
|
if (!usernameOrEmail || !password) {
|
|
setError('모든 필드를 입력해주세요.');
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
try {
|
|
await login(usernameOrEmail, password);
|
|
handleClose();
|
|
} catch (err: any) {
|
|
setError(err.response?.data?.error || '로그인에 실패했습니다. 아이디와 비밀번호를 확인해주세요.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleClose = () => {
|
|
setUsernameOrEmail('');
|
|
setPassword('');
|
|
setError('');
|
|
setShowPassword(false);
|
|
onClose();
|
|
};
|
|
|
|
// 테스트 계정으로 자동 로그인
|
|
const handleTestLogin = async (username: string, password: string) => {
|
|
setError('');
|
|
setIsLoading(true);
|
|
try {
|
|
await login(username, password);
|
|
handleClose();
|
|
} catch (err: any) {
|
|
setError(err.response?.data?.error || '로그인에 실패했습니다.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog
|
|
open={open}
|
|
onClose={handleClose}
|
|
maxWidth="xs"
|
|
fullWidth
|
|
hideBackdrop={true}
|
|
disableScrollLock={true}
|
|
PaperComponent={DraggablePaper}
|
|
sx={{
|
|
pointerEvents: 'none',
|
|
'& .MuiDialog-container': {
|
|
pointerEvents: 'none',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
}}
|
|
PaperProps={{
|
|
sx: { pointerEvents: 'auto' },
|
|
}}
|
|
>
|
|
<GoldenPopupDialogTitle badge="AUTH" title="LOGIN" titleKo="로그인" onClose={handleClose} />
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
<DialogContent>
|
|
{error && (
|
|
<Alert severity="error" sx={{ mb: 2 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="사용자명 또는 이메일"
|
|
value={usernameOrEmail}
|
|
onChange={(e) => setUsernameOrEmail(e.target.value)}
|
|
margin="normal"
|
|
autoFocus
|
|
disabled={isLoading}
|
|
/>
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="비밀번호"
|
|
type={showPassword ? 'text' : 'password'}
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
margin="normal"
|
|
disabled={isLoading}
|
|
InputProps={{
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<IconButton
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
edge="end"
|
|
>
|
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
|
</IconButton>
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
/>
|
|
|
|
<Box sx={{ mt: 2, textAlign: 'center' }}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
계정이 없으신가요?{' '}
|
|
<Link
|
|
component="button"
|
|
type="button"
|
|
onClick={() => {
|
|
handleClose();
|
|
onSwitchToSignUp();
|
|
}}
|
|
sx={{ cursor: 'pointer' }}
|
|
>
|
|
회원가입
|
|
</Link>
|
|
</Typography>
|
|
</Box>
|
|
</DialogContent>
|
|
|
|
<DialogActions sx={{ px: 3, pb: 2, flexDirection: 'column', gap: 2 }}>
|
|
<Box sx={{ display: 'flex', gap: 1, width: '100%' }}>
|
|
<Button
|
|
onClick={handleClose}
|
|
disabled={isLoading}
|
|
variant="outlined"
|
|
fullWidth
|
|
>
|
|
취소
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
variant="contained"
|
|
disabled={isLoading}
|
|
fullWidth
|
|
>
|
|
{isLoading ? '로그인 중...' : '로그인'}
|
|
</Button>
|
|
</Box>
|
|
|
|
<Divider sx={{ width: '100%' }}>
|
|
<Typography variant="caption" color="text.secondary">
|
|
테스트 계정
|
|
</Typography>
|
|
</Divider>
|
|
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, width: '100%' }}>
|
|
<Button
|
|
variant="outlined"
|
|
color="secondary"
|
|
startIcon={<AccountCircle />}
|
|
onClick={() => handleTestLogin('test1', 'test1')}
|
|
disabled={isLoading}
|
|
fullWidth
|
|
>
|
|
Test 계정1
|
|
</Button>
|
|
<Button
|
|
variant="outlined"
|
|
color="secondary"
|
|
startIcon={<AccountCircle />}
|
|
onClick={() => handleTestLogin('test2', 'test2')}
|
|
disabled={isLoading}
|
|
fullWidth
|
|
>
|
|
Test 계정2
|
|
</Button>
|
|
<Button
|
|
variant="outlined"
|
|
color="secondary"
|
|
startIcon={<AccountCircle />}
|
|
onClick={() => handleTestLogin('test3', 'test3')}
|
|
disabled={isLoading}
|
|
fullWidth
|
|
>
|
|
Test 계정3
|
|
</Button>
|
|
</Box>
|
|
</DialogActions>
|
|
</form>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
export default LoginDialog;
|
|
|